perbaikan tool generete

This commit is contained in:
2025-08-21 05:38:22 +07:00
parent 37890cd69a
commit fb41d7720e
12 changed files with 2917 additions and 1618 deletions

View File

@@ -80,9 +80,11 @@ tools/generate.bat product get post put delete
./tools/generate.sh product get post put delete
# Atau langsung dengan Go
go run tools/generate-handler.go product get post
go run tools/generate-handler.go orders get post
go run tools/generate-handler.go order get post put delete stats
go run tools/generate-handler.go orders/product get post
go run tools/generate-handler.go orders/order get post put delete dynamic search stats
go run tools/generate-bpjs-handler.go reference/peserta get
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
package middleware
import (
models "api-service/internal/models/retribusi"
models "api-service/internal/models"
"net/http"
"github.com/gin-gonic/gin"

85
internal/models/models.go Normal file
View File

@@ -0,0 +1,85 @@
package models
import (
"database/sql"
"database/sql/driver"
"time"
)
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
type NullableInt32 struct {
Int32 int32 `json:"int32,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableInt32
func (n *NullableInt32) Scan(value interface{}) error {
var ni sql.NullInt32
if err := ni.Scan(value); err != nil {
return err
}
n.Int32 = ni.Int32
n.Valid = ni.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableInt32
func (n NullableInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Int32, nil
}
// Metadata untuk pagination - dioptimalkan
type MetaResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Aggregate data untuk summary
type AggregateData struct {
TotalActive int `json:"total_active"`
TotalDraft int `json:"total_draft"`
TotalInactive int `json:"total_inactive"`
ByStatus map[string]int `json:"by_status"`
ByDinas map[string]int `json:"by_dinas,omitempty"`
ByJenis map[string]int `json:"by_jenis,omitempty"`
LastUpdated *time.Time `json:"last_updated,omitempty"`
CreatedToday int `json:"created_today"`
UpdatedToday int `json:"updated_today"`
}
// Error response yang konsisten
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
// Validation constants
const (
StatusDraft = "draft"
StatusActive = "active"
StatusInactive = "inactive"
StatusDeleted = "deleted"
)
// ValidStatuses untuk validasi
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
// IsValidStatus helper function
func IsValidStatus(status string) bool {
for _, validStatus := range ValidStatuses {
if status == validStatus {
return true
}
}
return false
}

View File

@@ -1,307 +1,229 @@
package models
import (
"api-service/internal/models"
"database/sql"
"database/sql/driver"
"encoding/json"
"time"
)
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
type NullableInt32 struct {
Int32 int32 `json:"int32,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableInt32
func (n *NullableInt32) Scan(value interface{}) error {
var ni sql.NullInt32
if err := ni.Scan(value); err != nil {
return err
}
n.Int32 = ni.Int32
n.Valid = ni.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableInt32
func (n NullableInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Int32, nil
}
// Retribusi represents the data structure for the retribusi table
// with proper null handling and optimized JSON marshaling
type Retribusi struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
Jenis sql.NullString `json:"jenis,omitempty" db:"Jenis"`
Pelayanan sql.NullString `json:"pelayanan,omitempty" db:"Pelayanan"`
Dinas sql.NullString `json:"dinas,omitempty" db:"Dinas"`
KelompokObyek sql.NullString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
KodeTarif sql.NullString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
Tarif sql.NullString `json:"tarif,omitempty" db:"Tarif"`
Satuan sql.NullString `json:"satuan,omitempty" db:"Satuan"`
TarifOvertime sql.NullString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
SatuanOvertime sql.NullString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
RekeningPokok sql.NullString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
RekeningDenda sql.NullString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
Uraian1 sql.NullString `json:"uraian_1,omitempty" db:"Uraian_1"`
Uraian2 sql.NullString `json:"uraian_2,omitempty" db:"Uraian_2"`
Uraian3 sql.NullString `json:"uraian_3,omitempty" db:"Uraian_3"`
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort models.NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
Jenis sql.NullString `json:"jenis,omitempty" db:"Jenis"`
Pelayanan sql.NullString `json:"pelayanan,omitempty" db:"Pelayanan"`
Dinas sql.NullString `json:"dinas,omitempty" db:"Dinas"`
KelompokObyek sql.NullString `json:"kelompok_obyek,omitempty" db:"Kelompok_obyek"`
KodeTarif sql.NullString `json:"kode_tarif,omitempty" db:"Kode_tarif"`
Tarif sql.NullString `json:"tarif,omitempty" db:"Tarif"`
Satuan sql.NullString `json:"satuan,omitempty" db:"Satuan"`
TarifOvertime sql.NullString `json:"tarif_overtime,omitempty" db:"Tarif_overtime"`
SatuanOvertime sql.NullString `json:"satuan_overtime,omitempty" db:"Satuan_overtime"`
RekeningPokok sql.NullString `json:"rekening_pokok,omitempty" db:"Rekening_pokok"`
RekeningDenda sql.NullString `json:"rekening_denda,omitempty" db:"Rekening_denda"`
Uraian1 sql.NullString `json:"uraian_1,omitempty" db:"Uraian_1"`
Uraian2 sql.NullString `json:"uraian_2,omitempty" db:"Uraian_2"`
Uraian3 sql.NullString `json:"uraian_3,omitempty" db:"Uraian_3"`
}
// Custom JSON marshaling untuk Retribusi agar NULL values tidak muncul di response
func (r Retribusi) MarshalJSON() ([]byte, error) {
type Alias Retribusi
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Jenis *string `json:"jenis,omitempty"`
Pelayanan *string `json:"pelayanan,omitempty"`
Dinas *string `json:"dinas,omitempty"`
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
KodeTarif *string `json:"kode_tarif,omitempty"`
Tarif *string `json:"tarif,omitempty"`
Satuan *string `json:"satuan,omitempty"`
TarifOvertime *string `json:"tarif_overtime,omitempty"`
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
RekeningPokok *string `json:"rekening_pokok,omitempty"`
RekeningDenda *string `json:"rekening_denda,omitempty"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
type Alias Retribusi
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Jenis *string `json:"jenis,omitempty"`
Pelayanan *string `json:"pelayanan,omitempty"`
Dinas *string `json:"dinas,omitempty"`
KelompokObyek *string `json:"kelompok_obyek,omitempty"`
KodeTarif *string `json:"kode_tarif,omitempty"`
Tarif *string `json:"tarif,omitempty"`
Satuan *string `json:"satuan,omitempty"`
TarifOvertime *string `json:"tarif_overtime,omitempty"`
SatuanOvertime *string `json:"satuan_overtime,omitempty"`
RekeningPokok *string `json:"rekening_pokok,omitempty"`
RekeningDenda *string `json:"rekening_denda,omitempty"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
// Convert NullableInt32 to pointer
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Jenis.Valid {
aux.Jenis = &r.Jenis.String
}
if r.Pelayanan.Valid {
aux.Pelayanan = &r.Pelayanan.String
}
if r.Dinas.Valid {
aux.Dinas = &r.Dinas.String
}
if r.KelompokObyek.Valid {
aux.KelompokObyek = &r.KelompokObyek.String
}
if r.KodeTarif.Valid {
aux.KodeTarif = &r.KodeTarif.String
}
if r.Tarif.Valid {
aux.Tarif = &r.Tarif.String
}
if r.Satuan.Valid {
aux.Satuan = &r.Satuan.String
}
if r.TarifOvertime.Valid {
aux.TarifOvertime = &r.TarifOvertime.String
}
if r.SatuanOvertime.Valid {
aux.SatuanOvertime = &r.SatuanOvertime.String
}
if r.RekeningPokok.Valid {
aux.RekeningPokok = &r.RekeningPokok.String
}
if r.RekeningDenda.Valid {
aux.RekeningDenda = &r.RekeningDenda.String
}
if r.Uraian1.Valid {
aux.Uraian1 = &r.Uraian1.String
}
if r.Uraian2.Valid {
aux.Uraian2 = &r.Uraian2.String
}
if r.Uraian3.Valid {
aux.Uraian3 = &r.Uraian3.String
}
// Convert NullableInt32 to pointer
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Jenis.Valid {
aux.Jenis = &r.Jenis.String
}
if r.Pelayanan.Valid {
aux.Pelayanan = &r.Pelayanan.String
}
if r.Dinas.Valid {
aux.Dinas = &r.Dinas.String
}
if r.KelompokObyek.Valid {
aux.KelompokObyek = &r.KelompokObyek.String
}
if r.KodeTarif.Valid {
aux.KodeTarif = &r.KodeTarif.String
}
if r.Tarif.Valid {
aux.Tarif = &r.Tarif.String
}
if r.Satuan.Valid {
aux.Satuan = &r.Satuan.String
}
if r.TarifOvertime.Valid {
aux.TarifOvertime = &r.TarifOvertime.String
}
if r.SatuanOvertime.Valid {
aux.SatuanOvertime = &r.SatuanOvertime.String
}
if r.RekeningPokok.Valid {
aux.RekeningPokok = &r.RekeningPokok.String
}
if r.RekeningDenda.Valid {
aux.RekeningDenda = &r.RekeningDenda.String
}
if r.Uraian1.Valid {
aux.Uraian1 = &r.Uraian1.String
}
if r.Uraian2.Valid {
aux.Uraian2 = &r.Uraian2.String
}
if r.Uraian3.Valid {
aux.Uraian3 = &r.Uraian3.String
}
return json.Marshal(aux)
return json.Marshal(aux)
}
// Helper methods untuk mendapatkan nilai yang aman
func (r *Retribusi) GetJenis() string {
if r.Jenis.Valid {
return r.Jenis.String
}
return ""
if r.Jenis.Valid {
return r.Jenis.String
}
return ""
}
func (r *Retribusi) GetDinas() string {
if r.Dinas.Valid {
return r.Dinas.String
}
return ""
if r.Dinas.Valid {
return r.Dinas.String
}
return ""
}
func (r *Retribusi) GetTarif() string {
if r.Tarif.Valid {
return r.Tarif.String
}
return ""
if r.Tarif.Valid {
return r.Tarif.String
}
return ""
}
// Response struct untuk GET by ID - diperbaiki struktur
type RetribusiGetByIDResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Request struct untuk create - dioptimalkan dengan validasi
type RetribusiCreateRequest struct {
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk create
type RetribusiCreateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Update request - sama seperti create tapi dengan ID
type RetribusiUpdateRequest struct {
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Jenis *string `json:"jenis,omitempty" validate:"omitempty,min=1,max=255"`
Pelayanan *string `json:"pelayanan,omitempty" validate:"omitempty,min=1,max=255"`
Dinas *string `json:"dinas,omitempty" validate:"omitempty,min=1,max=255"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" validate:"omitempty,min=1,max=255"`
KodeTarif *string `json:"kode_tarif,omitempty" validate:"omitempty,min=1,max=255"`
Uraian1 *string `json:"uraian_1,omitempty"`
Uraian2 *string `json:"uraian_2,omitempty"`
Uraian3 *string `json:"uraian_3,omitempty"`
Tarif *string `json:"tarif,omitempty" validate:"omitempty,numeric"`
Satuan *string `json:"satuan,omitempty" validate:"omitempty,min=1,max=255"`
TarifOvertime *string `json:"tarif_overtime,omitempty" validate:"omitempty,numeric"`
SatuanOvertime *string `json:"satuan_overtime,omitempty" validate:"omitempty,min=1,max=255"`
RekeningPokok *string `json:"rekening_pokok,omitempty" validate:"omitempty,min=1,max=255"`
RekeningDenda *string `json:"rekening_denda,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct untuk update
type RetribusiUpdateResponse struct {
Message string `json:"message"`
Data *Retribusi `json:"data"`
Message string `json:"message"`
Data *Retribusi `json:"data"`
}
// Response struct untuk delete
type RetribusiDeleteResponse struct {
Message string `json:"message"`
ID string `json:"id"`
Message string `json:"message"`
ID string `json:"id"`
}
// Enhanced GET response dengan pagination dan aggregation
type RetribusiGetResponse struct {
Message string `json:"message"`
Data []Retribusi `json:"data"`
Meta MetaResponse `json:"meta"`
Summary *AggregateData `json:"summary,omitempty"`
}
// Metadata untuk pagination - dioptimalkan
type MetaResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Aggregate data untuk summary
type AggregateData struct {
TotalActive int `json:"total_active"`
TotalDraft int `json:"total_draft"`
TotalInactive int `json:"total_inactive"`
ByStatus map[string]int `json:"by_status"`
ByDinas map[string]int `json:"by_dinas,omitempty"`
ByJenis map[string]int `json:"by_jenis,omitempty"`
LastUpdated *time.Time `json:"last_updated,omitempty"`
CreatedToday int `json:"created_today"`
UpdatedToday int `json:"updated_today"`
}
// Error response yang konsisten
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
Message string `json:"message"`
Data []Retribusi `json:"data"`
Meta models.MetaResponse `json:"meta"`
Summary *models.AggregateData `json:"summary,omitempty"`
}
// Filter struct untuk query parameters
type RetribusiFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Jenis *string `json:"jenis,omitempty" form:"jenis"`
Dinas *string `json:"dinas,omitempty" form:"dinas"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
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"`
}
// Validation constants
const (
StatusDraft = "draft"
StatusActive = "active"
StatusInactive = "inactive"
StatusDeleted = "deleted"
)
// ValidStatuses untuk validasi
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
// IsValidStatus helper function
func IsValidStatus(status string) bool {
for _, validStatus := range ValidStatuses {
if status == validStatus {
return true
}
}
return false
Status *string `json:"status,omitempty" form:"status"`
Jenis *string `json:"jenis,omitempty" form:"jenis"`
Dinas *string `json:"dinas,omitempty" form:"dinas"`
KelompokObyek *string `json:"kelompok_obyek,omitempty" form:"kelompok_obyek"`
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

@@ -55,7 +55,18 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
// BPJS endpoints
bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)
v1.GET("/bpjs/peserta/nik/:nik/tglSEP/:tglSEP", bpjsPesertaHandler.GetPesertaByNIK)
// Retribusi endpoints
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
retribusiGroup := v1.Group("/retribusi")
{
retribusiGroup.GET("", retribusiHandler.GetRetribusi)
retribusiGroup.GET("/dynamic", retribusiHandler.GetRetribusiDynamic) // Route baru
retribusiGroup.GET("/search", retribusiHandler.SearchRetribusiAdvanced) // Route pencarian
retribusiGroup.GET("/:id", retribusiHandler.GetRetribusiByID)
retribusiGroup.POST("", retribusiHandler.CreateRetribusi)
retribusiGroup.PUT("/:id", retribusiHandler.UpdateRetribusi)
retribusiGroup.DELETE("/:id", retribusiHandler.DeleteRetribusi)
}
// =============================================================================
// PROTECTED ROUTES (Authentication Required)
// =============================================================================
@@ -68,15 +79,15 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
protected.GET("/auth/me", authHandler.Me)
// Retribusi endpoints (CRUD operations - should be protected)
retribusiHandler := retribusiHandlers.NewRetribusiHandler()
protectedRetribusi := protected.Group("/retribusi")
{
protectedRetribusi.GET("/", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi/
protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
}
// retribusiHandler := retribusiHandlers.NewRetribusiHandler()
// protectedRetribusi := protected.Group("/retribusi")
// {
// protectedRetribusi.GET("", retribusiHandler.GetRetribusi) // GET /api/v1/retribusi
// protectedRetribusi.GET("/:id", retribusiHandler.GetRetribusiByID) // GET /api/v1/retribusi/:id
// protectedRetribusi.POST("/", retribusiHandler.CreateRetribusi) // POST /api/v1/retribusi/
// protectedRetribusi.PUT("/:id", retribusiHandler.UpdateRetribusi) // PUT /api/v1/retribusi/:id
// protectedRetribusi.DELETE("/:id", retribusiHandler.DeleteRetribusi) // DELETE /api/v1/retribusi/:id
// }
// BPJS endpoints (sensitive data - should be protected)
// bpjsPesertaHandler := bpjsPesertaHandlers.NewPesertaHandler(cfg.Bpjs)

View File

@@ -1,42 +0,0 @@
package utils
import (
"os"
"strconv"
"time"
)
// GetEnv retrieves environment variable with fallback
func GetEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// GetEnvAsInt retrieves environment variable as integer with fallback
func GetEnvAsInt(key string, defaultValue int) int {
valueStr := GetEnv(key, "")
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultValue
}
// GetEnvAsBool retrieves environment variable as boolean with fallback
func GetEnvAsBool(key string, defaultValue bool) bool {
valueStr := GetEnv(key, "")
if value, err := strconv.ParseBool(valueStr); err == nil {
return value
}
return defaultValue
}
// GetEnvAsDuration retrieves environment variable as duration with fallback
func GetEnvAsDuration(key string, defaultValue time.Duration) time.Duration {
valueStr := GetEnv(key, "")
if value, err := time.ParseDuration(valueStr); err == nil {
return value
}
return defaultValue
}

View File

@@ -0,0 +1,541 @@
package utils
import (
"fmt"
"reflect"
"strings"
)
// FilterOperator represents supported filter operators
type FilterOperator string
const (
OpEqual FilterOperator = "_eq"
OpNotEqual FilterOperator = "_neq"
OpLike FilterOperator = "_like"
OpILike FilterOperator = "_ilike"
OpIn FilterOperator = "_in"
OpNotIn FilterOperator = "_nin"
OpGreaterThan FilterOperator = "_gt"
OpGreaterThanEqual FilterOperator = "_gte"
OpLessThan FilterOperator = "_lt"
OpLessThanEqual FilterOperator = "_lte"
OpBetween FilterOperator = "_between"
OpNotBetween FilterOperator = "_nbetween"
OpNull FilterOperator = "_null"
OpNotNull FilterOperator = "_nnull"
OpContains FilterOperator = "_contains"
OpNotContains FilterOperator = "_ncontains"
OpStartsWith FilterOperator = "_starts_with"
OpEndsWith FilterOperator = "_ends_with"
)
// DynamicFilter represents a single filter condition
type DynamicFilter struct {
Column string `json:"column"`
Operator FilterOperator `json:"operator"`
Value interface{} `json:"value"`
LogicOp string `json:"logic_op,omitempty"` // AND, OR
}
// FilterGroup represents a group of filters
type FilterGroup struct {
Filters []DynamicFilter `json:"filters"`
LogicOp string `json:"logic_op"` // AND, OR
}
// DynamicQuery represents the complete query structure
type DynamicQuery struct {
Fields []string `json:"fields,omitempty"`
Filters []FilterGroup `json:"filters,omitempty"`
Sort []SortField `json:"sort,omitempty"`
Limit int `json:"limit"`
Offset int `json:"offset"`
GroupBy []string `json:"group_by,omitempty"`
Having []FilterGroup `json:"having,omitempty"`
}
// SortField represents sorting configuration
type SortField struct {
Column string `json:"column"`
Order string `json:"order"` // ASC, DESC
}
// QueryBuilder builds SQL queries from dynamic filters
type QueryBuilder struct {
tableName string
columnMapping map[string]string // Maps API field names to DB column names
allowedColumns map[string]bool // Security: only allow specified columns
paramCounter int
}
// NewQueryBuilder creates a new query builder instance
func NewQueryBuilder(tableName string) *QueryBuilder {
return &QueryBuilder{
tableName: tableName,
columnMapping: make(map[string]string),
allowedColumns: make(map[string]bool),
paramCounter: 0,
}
}
// SetColumnMapping sets the mapping between API field names and database column names
func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilder {
qb.columnMapping = mapping
return qb
}
// SetAllowedColumns sets the list of allowed columns for security
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
qb.allowedColumns = make(map[string]bool)
for _, col := range columns {
qb.allowedColumns[col] = true
}
return qb
}
// BuildQuery builds the complete SQL query
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
qb.paramCounter = 0
// Build SELECT clause
selectClause := qb.buildSelectClause(query.Fields)
// Build FROM clause
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
if err != nil {
return "", nil, err
}
// Build ORDER BY clause
orderClause := qb.buildOrderClause(query.Sort)
// Build GROUP BY clause
groupClause := qb.buildGroupByClause(query.GroupBy)
// Build HAVING clause
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
if err != nil {
return "", nil, err
}
// Combine all parts
sqlParts := []string{selectClause, fromClause}
args := []interface{}{}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
args = append(args, whereArgs...)
}
if groupClause != "" {
sqlParts = append(sqlParts, groupClause)
}
if havingClause != "" {
sqlParts = append(sqlParts, "HAVING "+havingClause)
args = append(args, havingArgs...)
}
if orderClause != "" {
sqlParts = append(sqlParts, orderClause)
}
// Add pagination
if query.Limit > 0 {
qb.paramCounter++
sqlParts = append(sqlParts, fmt.Sprintf("LIMIT $%d", qb.paramCounter))
args = append(args, query.Limit)
}
if query.Offset > 0 {
qb.paramCounter++
sqlParts = append(sqlParts, fmt.Sprintf("OFFSET $%d", qb.paramCounter))
args = append(args, query.Offset)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// buildSelectClause builds the SELECT part of the query
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
return "SELECT *"
}
var selectedFields []string
for _, field := range fields {
if field == "*.*" || field == "*" {
selectedFields = append(selectedFields, "*")
continue
}
// Map field name if mapping exists
if mappedCol, exists := qb.columnMapping[field]; exists {
field = mappedCol
}
// Security check: only allow specified columns
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[field] {
continue
}
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, field))
}
if len(selectedFields) == 0 {
return "SELECT *"
}
return "SELECT " + strings.Join(selectedFields, ", ")
}
// buildWhereClause builds the WHERE part of the query
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup) (string, []interface{}, error) {
if len(filterGroups) == 0 {
return "", nil, nil
}
var conditions []string
var args []interface{}
for i, group := range filterGroups {
groupCondition, groupArgs, err := qb.buildFilterGroup(group)
if err != nil {
return "", nil, err
}
if groupCondition != "" {
if i > 0 {
logicOp := "AND"
if group.LogicOp != "" {
logicOp = strings.ToUpper(group.LogicOp)
}
conditions = append(conditions, logicOp)
}
conditions = append(conditions, "("+groupCondition+")")
args = append(args, groupArgs...)
}
}
return strings.Join(conditions, " "), args, nil
}
// buildFilterGroup builds conditions for a filter group
func (qb *QueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface{}, error) {
if len(group.Filters) == 0 {
return "", nil, nil
}
var conditions []string
var args []interface{}
for i, filter := range group.Filters {
condition, filterArgs, err := qb.buildFilterCondition(filter)
if err != nil {
return "", nil, err
}
if condition != "" {
if i > 0 {
logicOp := "AND"
if filter.LogicOp != "" {
logicOp = strings.ToUpper(filter.LogicOp)
} else if group.LogicOp != "" {
logicOp = strings.ToUpper(group.LogicOp)
}
conditions = append(conditions, logicOp)
}
conditions = append(conditions, condition)
args = append(args, filterArgs...)
}
}
return strings.Join(conditions, " "), args, nil
}
// buildFilterCondition builds a single filter condition
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []interface{}, error) {
// Map column name if mapping exists
column := filter.Column
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
return "", nil, fmt.Errorf("column '%s' is not allowed", filter.Column)
}
// Wrap column name in quotes for PostgreSQL
column = fmt.Sprintf(`"%s"`, column)
switch filter.Operator {
case OpEqual:
qb.paramCounter++
return fmt.Sprintf("%s = $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpNotEqual:
qb.paramCounter++
return fmt.Sprintf("%s != $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLike:
qb.paramCounter++
return fmt.Sprintf("%s LIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpILike:
qb.paramCounter++
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpIn:
values := qb.parseArrayValue(filter.Value)
if len(values) == 0 {
return "", nil, nil
}
var placeholders []string
var args []interface{}
for _, val := range values {
qb.paramCounter++
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
args = append(args, val)
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
case OpNotIn:
values := qb.parseArrayValue(filter.Value)
if len(values) == 0 {
return "", nil, nil
}
var placeholders []string
var args []interface{}
for _, val := range values {
qb.paramCounter++
placeholders = append(placeholders, fmt.Sprintf("$%d", qb.paramCounter))
args = append(args, val)
}
return fmt.Sprintf("%s NOT IN (%s)", column, strings.Join(placeholders, ", ")), args, nil
case OpGreaterThan:
qb.paramCounter++
return fmt.Sprintf("%s > $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpGreaterThanEqual:
qb.paramCounter++
return fmt.Sprintf("%s >= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLessThan:
qb.paramCounter++
return fmt.Sprintf("%s < $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpLessThanEqual:
qb.paramCounter++
return fmt.Sprintf("%s <= $%d", column, qb.paramCounter), []interface{}{filter.Value}, nil
case OpBetween:
values := qb.parseArrayValue(filter.Value)
if len(values) != 2 {
return "", nil, fmt.Errorf("between operator requires exactly 2 values")
}
qb.paramCounter++
param1 := qb.paramCounter
qb.paramCounter++
param2 := qb.paramCounter
return fmt.Sprintf("%s BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
case OpNotBetween:
values := qb.parseArrayValue(filter.Value)
if len(values) != 2 {
return "", nil, fmt.Errorf("not between operator requires exactly 2 values")
}
qb.paramCounter++
param1 := qb.paramCounter
qb.paramCounter++
param2 := qb.paramCounter
return fmt.Sprintf("%s NOT BETWEEN $%d AND $%d", column, param1, param2), []interface{}{values[0], values[1]}, nil
case OpNull:
return fmt.Sprintf("%s IS NULL", column), nil, nil
case OpNotNull:
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
case OpContains:
qb.paramCounter++
value := fmt.Sprintf("%%%v%%", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpNotContains:
qb.paramCounter++
value := fmt.Sprintf("%%%v%%", filter.Value)
return fmt.Sprintf("%s NOT ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpStartsWith:
qb.paramCounter++
value := fmt.Sprintf("%v%%", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
case OpEndsWith:
qb.paramCounter++
value := fmt.Sprintf("%%%v", filter.Value)
return fmt.Sprintf("%s ILIKE $%d", column, qb.paramCounter), []interface{}{value}, nil
default:
return "", nil, fmt.Errorf("unsupported operator: %s", filter.Operator)
}
}
// parseArrayValue parses array values from various formats
func (qb *QueryBuilder) parseArrayValue(value interface{}) []interface{} {
if value == nil {
return nil
}
// If it's already a slice
if reflect.TypeOf(value).Kind() == reflect.Slice {
v := reflect.ValueOf(value)
result := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
result[i] = v.Index(i).Interface()
}
return result
}
// If it's a string, try to split by comma
if str, ok := value.(string); ok {
if strings.Contains(str, ",") {
parts := strings.Split(str, ",")
result := make([]interface{}, len(parts))
for i, part := range parts {
result[i] = strings.TrimSpace(part)
}
return result
}
return []interface{}{str}
}
return []interface{}{value}
}
// buildOrderClause builds the ORDER BY clause
func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
if len(sortFields) == 0 {
return ""
}
var orderParts []string
for _, sort := range sortFields {
column := sort.Column
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
continue
}
order := "ASC"
if sort.Order != "" {
order = strings.ToUpper(sort.Order)
}
orderParts = append(orderParts, fmt.Sprintf(`"%s" %s`, column, order))
}
if len(orderParts) == 0 {
return ""
}
return "ORDER BY " + strings.Join(orderParts, ", ")
}
// buildGroupByClause builds the GROUP BY clause
func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string {
if len(groupFields) == 0 {
return ""
}
var groupParts []string
for _, field := range groupFields {
column := field
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
continue
}
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
}
if len(groupParts) == 0 {
return ""
}
return "GROUP BY " + strings.Join(groupParts, ", ")
}
// buildHavingClause builds the HAVING clause
func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup) (string, []interface{}, error) {
if len(havingGroups) == 0 {
return "", nil, nil
}
return qb.buildWhereClause(havingGroups)
}
// BuildCountQuery builds a count query
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
qb.paramCounter = 0
// Build FROM clause
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters)
if err != nil {
return "", nil, err
}
// Build GROUP BY clause
groupClause := qb.buildGroupByClause(query.GroupBy)
// Build HAVING clause
havingClause, havingArgs, err := qb.buildHavingClause(query.Having)
if err != nil {
return "", nil, err
}
// Combine parts
sqlParts := []string{"SELECT COUNT(*)", fromClause}
args := []interface{}{}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
args = append(args, whereArgs...)
}
if groupClause != "" {
sqlParts = append(sqlParts, groupClause)
}
if havingClause != "" {
sqlParts = append(sqlParts, "HAVING "+havingClause)
args = append(args, havingArgs...)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}

View File

@@ -0,0 +1,241 @@
package utils
import (
"net/url"
"strconv"
"strings"
"time"
)
// QueryParser parses HTTP query parameters into DynamicQuery
type QueryParser struct {
defaultLimit int
maxLimit int
}
// NewQueryParser creates a new query parser
func NewQueryParser() *QueryParser {
return &QueryParser{
defaultLimit: 10,
maxLimit: 100,
}
}
// SetLimits sets default and maximum limits
func (qp *QueryParser) SetLimits(defaultLimit, maxLimit int) *QueryParser {
qp.defaultLimit = defaultLimit
qp.maxLimit = maxLimit
return qp
}
// ParseQuery parses URL query parameters into DynamicQuery
func (qp *QueryParser) ParseQuery(values url.Values) (DynamicQuery, error) {
query := DynamicQuery{
Limit: qp.defaultLimit,
Offset: 0,
}
// Parse fields
if fields := values.Get("fields"); fields != "" {
if fields == "*.*" || fields == "*" {
query.Fields = []string{"*"}
} else {
query.Fields = strings.Split(fields, ",")
for i, field := range query.Fields {
query.Fields[i] = strings.TrimSpace(field)
}
}
}
// Parse pagination
if limit := values.Get("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
if l > 0 && l <= qp.maxLimit {
query.Limit = l
}
}
}
if offset := values.Get("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
query.Offset = o
}
}
// Parse filters
filters, err := qp.parseFilters(values)
if err != nil {
return query, err
}
query.Filters = filters
// Parse sorting
sorts, err := qp.parseSorting(values)
if err != nil {
return query, err
}
query.Sort = sorts
// Parse group by
if groupBy := values.Get("group"); groupBy != "" {
query.GroupBy = strings.Split(groupBy, ",")
for i, field := range query.GroupBy {
query.GroupBy[i] = strings.TrimSpace(field)
}
}
return query, nil
}
// parseFilters parses filter parameters
// Supports format: filter[column][operator]=value
func (qp *QueryParser) parseFilters(values url.Values) ([]FilterGroup, error) {
filterMap := make(map[string]map[string]string)
// Group filters by column
for key, vals := range values {
if strings.HasPrefix(key, "filter[") && strings.HasSuffix(key, "]") {
// Parse filter[column][operator] format
parts := strings.Split(key[7:len(key)-1], "][")
if len(parts) == 2 {
column := parts[0]
operator := parts[1]
if filterMap[column] == nil {
filterMap[column] = make(map[string]string)
}
if len(vals) > 0 {
filterMap[column][operator] = vals[0]
}
}
}
}
if len(filterMap) == 0 {
return nil, nil
}
// Convert to FilterGroup
var filters []DynamicFilter
for column, operators := range filterMap {
for opStr, value := range operators {
operator := FilterOperator(opStr)
// Parse value based on operator
var parsedValue interface{}
switch operator {
case OpIn, OpNotIn:
if value != "" {
parsedValue = strings.Split(value, ",")
}
case OpBetween, OpNotBetween:
if value != "" {
parts := strings.Split(value, ",")
if len(parts) == 2 {
parsedValue = []interface{}{strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])}
}
}
case OpNull, OpNotNull:
parsedValue = nil
default:
parsedValue = value
}
filters = append(filters, DynamicFilter{
Column: column,
Operator: operator,
Value: parsedValue,
})
}
}
if len(filters) == 0 {
return nil, nil
}
return []FilterGroup{{
Filters: filters,
LogicOp: "AND",
}}, nil
}
// parseSorting parses sort parameters
// Supports format: sort=column1,-column2 (- for DESC)
func (qp *QueryParser) parseSorting(values url.Values) ([]SortField, error) {
sortParam := values.Get("sort")
if sortParam == "" {
return nil, nil
}
var sorts []SortField
fields := strings.Split(sortParam, ",")
for _, field := range fields {
field = strings.TrimSpace(field)
if field == "" {
continue
}
order := "ASC"
column := field
if strings.HasPrefix(field, "-") {
order = "DESC"
column = field[1:]
} else if strings.HasPrefix(field, "+") {
column = field[1:]
}
sorts = append(sorts, SortField{
Column: column,
Order: order,
})
}
return sorts, nil
}
// ParseAdvancedFilters parses complex filter structures
// Supports nested filters and logic operators
func (qp *QueryParser) ParseAdvancedFilters(filterParam string) ([]FilterGroup, error) {
// This would be for more complex JSON-based filters
// Implementation depends on your specific needs
return nil, nil
}
// Helper function to parse date values
func parseDate(value string) (interface{}, error) {
// Try different date formats
formats := []string{
"2006-01-02",
"2006-01-02T15:04:05Z",
"2006-01-02T15:04:05.000Z",
"2006-01-02 15:04:05",
}
for _, format := range formats {
if t, err := time.Parse(format, value); err == nil {
return t, nil
}
}
return value, nil
}
// Helper function to parse numeric values
func parseNumeric(value string) interface{} {
// Try integer first
if i, err := strconv.Atoi(value); err == nil {
return i
}
// Try float
if f, err := strconv.ParseFloat(value, 64); err == nil {
return f
}
// Return as string
return value
}

View File

@@ -1,220 +0,0 @@
<?php
if (!function_exists('formCreateData')) {
function formCreateData($data = [])
{
return [
"request" => [
"t_sep" => [
"noKartu" => $data['noKartu'],
"tglSep" => $data['tglSep'],
"ppkPelayanan" => $data['ppkPelayanan'],
"jnsPelayanan" => $data['jnsPelayanan'],
"klsRawat" => [
"klsRawatHak" => $data['klsRawatHak'],
"klsRawatNaik" => $data['klsRawatNaik'],
"pembiayaan" => $data['pembiayaan'],
"penanggungJawab" => $data['penanggungJawab']
],
"noMR" => $data['noMR'],
"rujukan" => [
"asalRujukan" => $data['asalRujukan'],
"tglRujukan" => $data['tglRujukan'],
"noRujukan" => $data['noRujukan'],
"ppkRujukan" => $data['ppkRujukan']
],
"catatan" => $data['catatan'],
"diagAwal" => $data['diagAwal'],
"poli" => [
"tujuan" => $data['tujuan'],
"eksekutif" => $data['eksekutif']
],
"cob" => [
"cob" => $data['cob']
],
"katarak" => [
"katarak" => $data['katarak']
],
"jaminan" => [
"lakaLantas" => $data['lakaLantas'],
"noLP" => $data['noLP'],
"penjamin" => [
"tglKejadian" => $data['tglKejadian'],
"keterangan" => $data['keterangan'],
"suplesi" => [
"suplesi" => $data['suplesi'],
"noSepSuplesi" => $data['noSepSuplesi'],
"lokasiLaka" => [
"kdPropinsi" => $data['kdPropinsi'],
"kdKabupaten" => $data['kdKabupaten'],
"kdKecamatan" => $data['kdKecamatan']
]
]
]
],
"tujuanKunj" => $data['tujuanKunj'],
"flagProcedure" => $data['flagProcedure'],
"kdPenunjang" => $data['kdPenunjang'],
"assesmentPel" => $data['assesmentPel'],
"skdp" => [
"noSurat" => $data['noSurat'],
"kodeDPJP" => $data['kodeDPJP']
],
"dpjpLayan" => $data['dpjpLayan'],
"noTelp" => $data['noTelp'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formUpdateData')) {
function formUpdateData($data = [])
{
return [
"request" => [
"t_sep" => [
"noSep" => $data['noSep'],
"klsRawat" => [
"klsRawatHak" => $data['klsRawatHak'],
"klsRawatNaik" => $data['klsRawatNaik'],
"pembiayaan" => $data['pembiayaan'],
"penanggungJawab" => $data['penanggungJawab']
],
"noMR" => $data['noMR'],
"catatan" => $data['catatan'],
"diagAwal" => $data['diagAwal'],
"poli" => [
"tujuan" => $data['tujuan'],
"eksekutif" => $data['eksekutif']
],
"cob" => [
"cob" => $data['cob']
],
"katarak" => [
"katarak" => $data['katarak']
],
"jaminan" => [
"lakaLantas" => $data['lakaLantas'],
"penjamin" => [
"tglKejadian" => $data['tglKejadian'],
"keterangan" => $data['keterangan'],
"suplesi" => [
"suplesi" => $data['suplesi'],
"noSepSuplesi" => $data['noSepSuplesi'],
"lokasiLaka" => [
"kdPropinsi" => $data['kdPropinsi'],
"kdKabupaten" => $data['kdKabupaten'],
"kdKecamatan" => $data['kdKecamatan']
]
]
]
],
"dpjpLayan" => $data['dpjpLayan'],
"noTelp" => $data['noTelp'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formDeleteData')) {
function formDeleteData($data = [])
{
return [
"request" => [
"t_sep" => [
"noSep" => $data['noSep'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formPengajuanData')) {
function formPengajuanData($data = [])
{
return [
"request" => [
"t_sep" => [
"noKartu" => $data['noKartu'],
"tglSep" => $data['tglSep'],
"jnsPelayanan" => $data['jnsPelayanan'],
"jnsPengajuan" => $data['jnsPengajuan'],
"keterangan" => $data['keterangan'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formAprovalPengajuanData')) {
function formAprovalPengajuanData($data = [])
{
return [
"request" => [
"t_sep" => [
"noKartu" => $data['noKartu'],
"tglSep" => $data['tglSep'],
"jnsPelayanan" => $data['jnsPelayanan'],
"jnsPengajuan" => $data['jnsPengajuan'],
"keterangan" => $data['keterangan'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formTanggalPulangData')) {
function formTanggalPulangData($data = [])
{
return [
"request" => [
"t_sep" => [
"noSep" => $data['noSep'],
"statusPulang" => $data['statusPulang'],
"noSuratMeninggal" => $data['noSuratMeninggal'],
"tglMeninggal" => $data['tglMeninggal'],
"tglPulang" => $data['tglPulang'],
"noLPManual" => $data['noLPManual'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formDeleteSepinternalData')) {
function formDeleteSepinternalData($data = [])
{
return [
"request" => [
"t_sep" => [
"noSep" => $data['noSep'],
"noSurat" => $data['noSurat'],
"tglRujukanInternal" => $data['tglRujukanInternal'],
"kdPoliTuj" => $data['kdPoliTuj'],
"user" => $data['user']
]
]
];
}
}
if (!function_exists('formRandomAnswerData')) {
function formRandomAnswerData($data = [])
{
return [
"request" => [
"t_sep" => [
"noKartu" => $data['noKartu'],
"tglSep" => $data['tglSep'],
"jenPel" => $data['jenPel'],
"ppkPelSep" => $data['ppkPelSep'],
"tglLahir" => $data['tglLahir'],
"ppkPst" => $data['ppkPst'],
"user" => $data['user']
]
]
];
}
}

View File

@@ -0,0 +1,141 @@
package validation
import (
"context"
"database/sql"
"fmt"
"time"
)
// ValidationConfig holds configuration for duplicate validation
type ValidationConfig struct {
TableName string
IDColumn string
StatusColumn string
DateColumn string
ActiveStatuses []string
AdditionalFields map[string]interface{}
}
// DuplicateValidator provides methods for validating duplicate entries
type DuplicateValidator struct {
db *sql.DB
}
// NewDuplicateValidator creates a new instance of DuplicateValidator
func NewDuplicateValidator(db *sql.DB) *DuplicateValidator {
return &DuplicateValidator{db: db}
}
// ValidateDuplicate checks for duplicate entries based on the provided configuration
func (dv *DuplicateValidator) ValidateDuplicate(ctx context.Context, config ValidationConfig, identifier interface{}) error {
query := fmt.Sprintf(`
SELECT COUNT(*)
FROM %s
WHERE %s = $1
AND %s = ANY($2)
AND DATE(%s) = CURRENT_DATE
`, config.TableName, config.IDColumn, config.StatusColumn, config.DateColumn)
var count int
err := dv.db.QueryRowContext(ctx, query, identifier, config.ActiveStatuses).Scan(&count)
if err != nil {
return fmt.Errorf("failed to check duplicate: %w", err)
}
if count > 0 {
return fmt.Errorf("data with ID %v already exists with active status today", identifier)
}
return nil
}
// 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
// Add additional field conditions
for fieldName, fieldValue := range config.AdditionalFields {
whereClause += fmt.Sprintf(" AND %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++
}
query := fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s", config.TableName, whereClause)
var count int
err := dv.db.QueryRowContext(ctx, query, args...).Scan(&count)
if err != nil {
return fmt.Errorf("failed to check duplicate with custom fields: %w", err)
}
if count > 0 {
return fmt.Errorf("duplicate entry found with the specified criteria")
}
return nil
}
// ValidateOncePerDay ensures only one submission per day for a given identifier
func (dv *DuplicateValidator) ValidateOncePerDay(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) error {
query := fmt.Sprintf(`
SELECT COUNT(*)
FROM %s
WHERE %s = $1
AND DATE(%s) = CURRENT_DATE
`, tableName, idColumn, dateColumn)
var count int
err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&count)
if err != nil {
return fmt.Errorf("failed to check daily submission: %w", err)
}
if count > 0 {
return fmt.Errorf("only one submission allowed per day for ID %v", identifier)
}
return nil
}
// GetLastSubmissionTime returns the last submission time for a given identifier
func (dv *DuplicateValidator) GetLastSubmissionTime(ctx context.Context, tableName, idColumn, dateColumn string, identifier interface{}) (*time.Time, error) {
query := fmt.Sprintf(`
SELECT %s
FROM %s
WHERE %s = $1
ORDER BY %s DESC
LIMIT 1
`, dateColumn, tableName, idColumn, dateColumn)
var lastTime time.Time
err := dv.db.QueryRowContext(ctx, query, identifier).Scan(&lastTime)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // No previous submission
}
return nil, fmt.Errorf("failed to get last submission time: %w", err)
}
return &lastTime, nil
}
// DefaultRetribusiConfig returns default configuration for retribusi validation
func DefaultRetribusiConfig() ValidationConfig {
return ValidationConfig{
TableName: "data_retribusi",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
}
}

File diff suppressed because it is too large Load Diff