Files
antrean-anjungan/internal/handlers/retribusi/retribusi.go
2025-10-23 04:25:28 +07:00

1418 lines
41 KiB
Go

package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models"
"api-service/internal/models/retribusi"
utils "api-service/internal/utils/filters"
"api-service/internal/utils/validation"
"api-service/pkg/logger"
"context"
"database/sql"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
"gorm.io/gorm"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
// Initialize the database connection and validator
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("retribusi_status", validateRetribusiStatus)
if db == nil {
logger.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for retribusi status
func validateRetribusiStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// RetribusiHandler handles retribusi services
type RetribusiHandler struct {
db database.Service
}
// NewRetribusiHandler creates a new RetribusiHandler
func NewRetribusiHandler() *RetribusiHandler {
return &RetribusiHandler{
db: db,
}
}
// GetRetribusi godoc
// @Summary Get retribusi with pagination and optional aggregation
// @Description Returns a paginated list of retribusis with optional summary statistics
// @Tags Retribusi
// @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 jenis query string false "Filter by jenis"
// @Param dinas query string false "Filter by dinas"
// @Param search query string false "Search in multiple fields"
// @Success 200 {object} retribusi.RetribusiGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusis [get]
func (h *RetribusiHandler) GetRetribusi(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
gormDB, err := h.db.GetGormDB("postgres_satudata")
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 (
retribusis []retribusi.Retribusi
total int64
aggregateData *models.AggregateData
wg sync.WaitGroup
errChan = make(chan error, 3)
mu sync.Mutex
)
// Get underlying SQL DB from GORM
sqlDB, err := gormDB.DB()
if err != nil {
h.logAndRespondError(c, "Failed to get SQL DB from GORM", err, http.StatusInternalServerError)
return
}
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
if err := h.getTotalCount(ctx, gormDB, 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.fetchRetribusis(ctx, sqlDB, filter, limit, offset)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to fetch data: %w", err)
} else {
retribusis = result
}
mu.Unlock()
}()
// Fetch aggregation data if requested
if includeAggregation {
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.getAggregateData(ctx, sqlDB, 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, int(total))
response := retribusi.RetribusiGetResponse{
Message: "Data retribusi berhasil diambil",
Data: retribusis,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetRetribusiByID godoc
// @Summary Get Retribusi by ID
// @Description Returns a single retribusi by ID
// @Tags Retribusi
// @Accept json
// @Produce json
// @Param id path string true "Retribusi ID (UUID)"
// @Success 200 {object} retribusi.RetribusiGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Retribusi not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusi/{id} [get]
func (h *RetribusiHandler) GetRetribusiByID(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
}
gormDB, err := h.db.GetGormDB("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()
dataretribusi, err := h.getRetribusiByID(ctx, gormDB, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Retribusi not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get retribusi", err, http.StatusInternalServerError)
}
return
}
response := retribusi.RetribusiGetByIDResponse{
Message: "Retribusi details retrieved successfully",
Data: dataretribusi,
}
c.JSON(http.StatusOK, response)
}
// GetRetribusiDynamic godoc
// @Summary Get retribusi with dynamic filtering
// @Description Returns retribusis with advanced dynamic filtering like Directus
// @Tags Retribusi
// @Accept json
// @Produce json
// @Param fields query string false "Fields to select (e.g., fields=*.*)"
// @Param filter[column][operator] query string false "Dynamic filters (e.g., filter[Jenis][_eq]=value)"
// @Param sort query string false "Sort fields (e.g., sort=date_created,-Jenis)"
// @Param limit query int false "Limit" default(10)
// @Param offset query int false "Offset" default(0)
// @Success 200 {object} retribusi.RetribusiGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusis/dynamic [get]
func (h *RetribusiHandler) GetRetribusiDynamic(c *gin.Context) {
// Parse query parameters
parser := utils.NewQueryParser().SetLimits(10, 100)
dynamicQuery, err := parser.ParseQuery(c.Request.URL.Query())
if err != nil {
h.respondError(c, "Invalid query parameters", err, http.StatusBadRequest)
return
}
// Get database connection
dbConn, err := h.db.GetDB("postgres_satudata")
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 query with dynamic filtering
retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, dynamicQuery)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(dynamicQuery.Limit, dynamicQuery.Offset, total)
response := retribusi.RetribusiGetResponse{
Message: "Data retribusi berhasil diambil",
Data: retribusis,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}
// fetchRetribusisDynamic executes dynamic query
func (h *RetribusiHandler) fetchRetribusisDynamic(ctx context.Context, dbConn *sql.DB, query utils.DynamicQuery) ([]retribusi.Retribusi, int, error) {
// Setup query builder
countBuilder := utils.NewQueryBuilder("data_retribusi").
SetColumnMapping(map[string]string{
"jenis": "Jenis",
"pelayanan": "Pelayanan",
"dinas": "Dinas",
"kelompok_obyek": "Kelompok_obyek",
"Kode_tarif": "Kode_tarif",
"kode_tarif": "Kode_tarif",
"tarif": "Tarif",
"satuan": "Satuan",
"tarif_overtime": "Tarif_overtime",
"satuan_overtime": "Satuan_overtime",
"rekening_pokok": "Rekening_pokok",
"rekening_denda": "Rekening_denda",
"uraian_1": "Uraian_1",
"uraian_2": "Uraian_2",
"uraian_3": "Uraian_3",
}).
SetAllowedColumns([]string{
"id", "status", "sort", "user_created", "date_created",
"user_updated", "date_updated", "Jenis", "Pelayanan",
"Dinas", "Kelompok_obyek", "Kode_tarif", "Tarif", "Satuan",
"Tarif_overtime", "Satuan_overtime", "Rekening_pokok",
"Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3",
})
mainBuilder := utils.NewQueryBuilder("data_retribusi").
SetColumnMapping(map[string]string{
"jenis": "Jenis",
"pelayanan": "Pelayanan",
"dinas": "Dinas",
"kelompok_obyek": "Kelompok_obyek",
"Kode_tarif": "Kode_tarif",
"kode_tarif": "Kode_tarif",
"tarif": "Tarif",
"satuan": "Satuan",
"tarif_overtime": "Tarif_overtime",
"satuan_overtime": "Satuan_overtime",
"rekening_pokok": "Rekening_pokok",
"rekening_denda": "Rekening_denda",
"uraian_1": "Uraian_1",
"uraian_2": "Uraian_2",
"uraian_3": "Uraian_3",
}).
SetAllowedColumns([]string{
"id", "status", "sort", "user_created", "date_created",
"user_updated", "date_updated", "Jenis", "Pelayanan",
"Dinas", "Kelompok_obyek", "Kode_tarif", "Tarif", "Satuan",
"Tarif_overtime", "Satuan_overtime", "Rekening_pokok",
"Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3",
})
// Add default filter to exclude deleted records
if len(query.Filters) > 0 {
query.Filters = append([]utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpNotEqual,
Value: "deleted",
}},
LogicOp: "AND",
}}, query.Filters...)
} else {
query.Filters = []utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "status",
Operator: utils.OpNotEqual,
Value: "deleted",
}},
LogicOp: "AND",
}}
}
// Execute queries sequentially to avoid race conditions
var total int
var retribusis []retribusi.Retribusi
// 1. Get total count first
countQuery := query
countQuery.Limit = 0
countQuery.Offset = 0
countSQL, countArgs, err := countBuilder.BuildCountQuery(countQuery)
if err != nil {
return nil, 0, fmt.Errorf("failed to build count query: %w", err)
}
if err := dbConn.QueryRowContext(ctx, countSQL, countArgs...).Scan(&total); err != nil {
return nil, 0, fmt.Errorf("failed to get total count: %w", err)
}
// 2. Get main data
mainSQL, mainArgs, err := mainBuilder.BuildQuery(query)
if err != nil {
return nil, 0, fmt.Errorf("failed to build main query: %w", err)
}
rows, err := dbConn.QueryContext(ctx, mainSQL, mainArgs...)
if err != nil {
return nil, 0, fmt.Errorf("failed to execute main query: %w", err)
}
defer rows.Close()
for rows.Next() {
retribusi, err := h.scanRetribusi(rows)
if err != nil {
return nil, 0, fmt.Errorf("failed to scan retribusi: %w", err)
}
retribusis = append(retribusis, retribusi)
}
if err := rows.Err(); err != nil {
return nil, 0, fmt.Errorf("rows iteration error: %w", err)
}
return retribusis, total, nil
}
// SearchRetribusiAdvanced provides advanced search capabilities
func (h *RetribusiHandler) SearchRetribusiAdvanced(c *gin.Context) {
// Parse complex search parameters
searchQuery := c.Query("q")
if searchQuery == "" {
// If no search query provided, return all records with default sorting
query := utils.DynamicQuery{
Fields: []string{"*"},
Filters: []utils.FilterGroup{}, // Empty filters - fetchRetribusisDynamic will add default deleted filter
Sort: []utils.SortField{{
Column: "date_created",
Order: "DESC",
}},
Limit: 20,
Offset: 0,
}
// Parse pagination if provided
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
query.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
query.Offset = o
}
}
// Get database connection
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(), 30*time.Second)
defer cancel()
// Execute query to get all records
retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Failed to fetch data", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, total)
response := retribusi.RetribusiGetResponse{
Message: "All records retrieved (no search query provided)",
Data: retribusis,
Meta: meta,
}
c.JSON(http.StatusOK, response)
return
}
// Build dynamic query for search
query := utils.DynamicQuery{
Fields: []string{"*"},
Filters: []utils.FilterGroup{{
Filters: []utils.DynamicFilter{
{
Column: "Jenis",
Operator: utils.OpContains,
Value: searchQuery,
LogicOp: "OR",
},
{
Column: "Pelayanan",
Operator: utils.OpContains,
Value: searchQuery,
LogicOp: "OR",
},
{
Column: "Dinas",
Operator: utils.OpContains,
Value: searchQuery,
LogicOp: "OR",
},
{
Column: "Uraian_1",
Operator: utils.OpContains,
Value: searchQuery,
LogicOp: "OR",
},
},
LogicOp: "AND",
}},
Sort: []utils.SortField{{
Column: "date_created",
Order: "DESC",
}},
Limit: 20,
Offset: 0,
}
// Parse pagination if provided
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
query.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil && o >= 0 {
query.Offset = o
}
}
// Get database connection
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(), 30*time.Second)
defer cancel()
// Execute search
retribusis, total, err := h.fetchRetribusisDynamic(ctx, dbConn, query)
if err != nil {
h.logAndRespondError(c, "Search failed", err, http.StatusInternalServerError)
return
}
// Build response
meta := h.calculateMeta(query.Limit, query.Offset, total)
response := retribusi.RetribusiGetResponse{
Message: fmt.Sprintf("Search results for '%s'", searchQuery),
Data: retribusis,
Meta: meta,
}
c.JSON(http.StatusOK, response)
}
// CreateRetribusi godoc
// @Summary Create retribusi
// @Description Creates a new retribusi record
// @Tags Retribusi
// @Accept json
// @Produce json
// @Param request body retribusi.RetribusiCreateRequest true "Retribusi creation request"
// @Success 201 {object} retribusi.RetribusiCreateResponse "Retribusi created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusis [post]
func (h *RetribusiHandler) CreateRetribusi(c *gin.Context) {
var req retribusi.RetribusiCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", 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()
// Validate duplicate and daily submission
if err := h.validateRetribusiSubmission(ctx, dbConn, &req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dataretribusi, err := h.createRetribusi(ctx, dbConn, &req)
if err != nil {
h.logAndRespondError(c, "Failed to create retribusi", err, http.StatusInternalServerError)
return
}
response := retribusi.RetribusiCreateResponse{
Message: "Retribusi berhasil dibuat",
Data: dataretribusi,
}
c.JSON(http.StatusCreated, response)
}
// UpdateRetribusi godoc
// @Summary Update retribusi
// @Description Updates an existing retribusi record
// @Tags Retribusi
// @Accept json
// @Produce json
// @Param id path string true "Retribusi ID (UUID)"
// @Param request body retribusi.RetribusiUpdateRequest true "Retribusi update request"
// @Success 200 {object} retribusi.RetribusiUpdateResponse "Retribusi updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Retribusi not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusi/{id} [put]
func (h *RetribusiHandler) UpdateRetribusi(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
}
var req retribusi.RetribusiUpdateRequest
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
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", 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()
dataretribusi, err := h.updateRetribusi(ctx, dbConn, &req)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Retribusi not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update retribusi", err, http.StatusInternalServerError)
}
return
}
response := retribusi.RetribusiUpdateResponse{
Message: "Retribusi berhasil diperbarui",
Data: dataretribusi,
}
c.JSON(http.StatusOK, response)
}
// DeleteRetribusi godoc
// @Summary Delete retribusi
// @Description Soft deletes a retribusi by setting status to 'deleted'
// @Tags Retribusi
// @Accept json
// @Produce json
// @Param id path string true "Retribusi ID (UUID)"
// @Success 200 {object} retribusi.RetribusiDeleteResponse "Retribusi deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Retribusi not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/retribusi/{id} [delete]
func (h *RetribusiHandler) DeleteRetribusi(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()
err = h.deleteRetribusi(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Retribusi not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to delete retribusi", err, http.StatusInternalServerError)
}
return
}
response := retribusi.RetribusiDeleteResponse{
Message: "Retribusi berhasil dihapus",
ID: id,
}
c.JSON(http.StatusOK, response)
}
// GetRetribusiStats godoc
// @Summary Get retribusi statistics
// @Description Returns comprehensive statistics about retribusi data
// @Tags Retribusi
// @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/retribusis/stats [get]
func (h *RetribusiHandler) GetRetribusiStats(c *gin.Context) {
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()
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 retribusi berhasil diambil",
"data": aggregateData,
})
}
// Get retribusi by ID using GORM
func (h *RetribusiHandler) getRetribusiByID(ctx context.Context, gormDB *gorm.DB, id string) (*retribusi.Retribusi, error) {
var retribusi retribusi.Retribusi
err := gormDB.WithContext(ctx).Where("id = ? AND status != ?", id, "deleted").First(&retribusi).Error
if err != nil {
return nil, err
}
return &retribusi, nil
}
// Create retribusi
func (h *RetribusiHandler) createRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) (*retribusi.Retribusi, error) {
id := uuid.New().String()
now := time.Now()
query := `
INSERT INTO data_retribusi (
id, status, date_created, date_updated,
"Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif",
"Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime",
"Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
RETURNING
id, status, sort, user_created, date_created, user_updated, date_updated,
"Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif",
"Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime",
"Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"`
row := dbConn.QueryRowContext(ctx, query,
id, req.Status, now, now,
req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif,
req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime,
req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3,
)
var retribusi retribusi.Retribusi
err := row.Scan(
&retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated,
&retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated,
&retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek,
&retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime,
&retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda,
&retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3,
)
if err != nil {
return nil, fmt.Errorf("failed to create retribusi: %w", err)
}
return &retribusi, nil
}
// Update retribusi
func (h *RetribusiHandler) updateRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiUpdateRequest) (*retribusi.Retribusi, error) {
now := time.Now()
query := `
UPDATE data_retribusi SET
status = $2, date_updated = $3,
"Jenis" = $4, "Pelayanan" = $5, "Dinas" = $6, "Kelompok_obyek" = $7, "Kode_tarif" = $8,
"Tarif" = $9, "Satuan" = $10, "Tarif_overtime" = $11, "Satuan_overtime" = $12,
"Rekening_pokok" = $13, "Rekening_denda" = $14, "Uraian_1" = $15, "Uraian_2" = $16, "Uraian_3" = $17
WHERE id = $1 AND status != 'deleted'
RETURNING
id, status, sort, user_created, date_created, user_updated, date_updated,
"Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif",
"Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime",
"Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"`
row := dbConn.QueryRowContext(ctx, query,
req.ID, req.Status, now,
req.Jenis, req.Pelayanan, req.Dinas, req.KelompokObyek, req.KodeTarif,
req.Tarif, req.Satuan, req.TarifOvertime, req.SatuanOvertime,
req.RekeningPokok, req.RekeningDenda, req.Uraian1, req.Uraian2, req.Uraian3,
)
var retribusi retribusi.Retribusi
err := row.Scan(
&retribusi.ID, &retribusi.Status, &retribusi.Sort, &retribusi.UserCreated,
&retribusi.DateCreated, &retribusi.UserUpdated, &retribusi.DateUpdated,
&retribusi.Jenis, &retribusi.Pelayanan, &retribusi.Dinas, &retribusi.KelompokObyek,
&retribusi.KodeTarif, &retribusi.Tarif, &retribusi.Satuan, &retribusi.TarifOvertime,
&retribusi.SatuanOvertime, &retribusi.RekeningPokok, &retribusi.RekeningDenda,
&retribusi.Uraian1, &retribusi.Uraian2, &retribusi.Uraian3,
)
if err != nil {
return nil, fmt.Errorf("failed to update retribusi: %w", err)
}
return &retribusi, nil
}
// Soft delete retribusi
func (h *RetribusiHandler) deleteRetribusi(ctx context.Context, dbConn *sql.DB, id string) error {
now := time.Now()
query := `UPDATE data_retribusi SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'`
result, err := dbConn.ExecContext(ctx, query, id, now)
if err != nil {
return fmt.Errorf("failed to delete retribusi: %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
}
// Enhanced error handling
func (h *RetribusiHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
logger.Error(message, map[string]interface{}{
"error": err.Error(),
"status_code": statusCode,
})
h.respondError(c, message, err, statusCode)
}
func (h *RetribusiHandler) 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 *RetribusiHandler) 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
}
logger.Debug("Pagination parameters", map[string]interface{}{
"limit": limit,
"offset": offset,
})
return limit, offset, nil
}
// Build WHERE clause dengan filter parameters
func (h *RetribusiHandler) buildWhereClause(filter retribusi.RetribusiFilter) (string, []interface{}) {
conditions := []string{"status != 'deleted'"}
args := []interface{}{}
paramCount := 1
if filter.Status != nil {
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
args = append(args, *filter.Status)
paramCount++
}
if filter.Jenis != nil {
conditions = append(conditions, fmt.Sprintf(`"Jenis" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.Jenis+"%")
paramCount++
}
if filter.Dinas != nil {
conditions = append(conditions, fmt.Sprintf(`"Dinas" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.Dinas+"%")
paramCount++
}
if filter.KelompokObyek != nil {
conditions = append(conditions, fmt.Sprintf(`"Kelompok_obyek" ILIKE $%d`, paramCount))
args = append(args, "%"+*filter.KelompokObyek+"%")
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf(`(
"Jenis" ILIKE $%d OR
"Pelayanan" ILIKE $%d OR
"Dinas" ILIKE $%d OR
"Kode_tarif" ILIKE $%d OR
"Uraian_1" ILIKE $%d OR
"Uraian_2" ILIKE $%d OR
"Uraian_3" ILIKE $%d
)`, paramCount, paramCount, paramCount, paramCount, paramCount, paramCount, 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)) // End of day
paramCount++
}
return strings.Join(conditions, " AND "), args
}
// Optimized scanning function yang menggunakan sql.Null* types langsung
func (h *RetribusiHandler) scanRetribusi(rows *sql.Rows) (retribusi.Retribusi, error) {
var retribusi retribusi.Retribusi
return retribusi, rows.Scan(
&retribusi.ID,
&retribusi.Status,
&retribusi.Sort,
&retribusi.UserCreated,
&retribusi.DateCreated,
&retribusi.UserUpdated,
&retribusi.DateUpdated,
&retribusi.Jenis,
&retribusi.Pelayanan,
&retribusi.Dinas,
&retribusi.KelompokObyek,
&retribusi.KodeTarif,
&retribusi.Tarif,
&retribusi.Satuan,
&retribusi.TarifOvertime,
&retribusi.SatuanOvertime,
&retribusi.RekeningPokok,
&retribusi.RekeningDenda,
&retribusi.Uraian1,
&retribusi.Uraian2,
&retribusi.Uraian3,
)
}
// Parse filter parameters dari query string
func (h *RetribusiHandler) parseFilterParams(c *gin.Context) retribusi.RetribusiFilter {
filter := retribusi.RetribusiFilter{}
if status := c.Query("status"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if jenis := c.Query("jenis"); jenis != "" {
filter.Jenis = &jenis
}
if dinas := c.Query("dinas"); dinas != "" {
filter.Dinas = &dinas
}
if kelompokObyek := c.Query("kelompok_obyek"); kelompokObyek != "" {
filter.KelompokObyek = &kelompokObyek
}
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
}
// Get comprehensive aggregate data dengan filter support
func (h *RetribusiHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
ByDinas: make(map[string]int),
ByJenis: 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 data_retribusi
WHERE %s
GROUP BY status
ORDER BY status`, 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. Count by Dinas
wg.Add(1)
go func() {
defer wg.Done()
dinasQuery := fmt.Sprintf(`
SELECT COALESCE("Dinas", 'Unknown') as dinas, COUNT(*)
FROM data_retribusi
WHERE %s AND "Dinas" IS NOT NULL AND TRIM("Dinas") != ''
GROUP BY "Dinas"
ORDER BY COUNT(*) DESC
LIMIT 10`, whereClause)
rows, err := dbConn.QueryContext(ctx, dinasQuery, args...)
if err != nil {
errChan <- fmt.Errorf("dinas query failed: %w", err)
return
}
defer rows.Close()
mu.Lock()
for rows.Next() {
var dinas string
var count int
if err := rows.Scan(&dinas, &count); err != nil {
mu.Unlock()
errChan <- fmt.Errorf("dinas scan failed: %w", err)
return
}
aggregate.ByDinas[dinas] = count
}
mu.Unlock()
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("dinas iteration error: %w", err)
}
}()
// 3. Count by Jenis
wg.Add(1)
go func() {
defer wg.Done()
jenisQuery := fmt.Sprintf(`
SELECT COALESCE("Jenis", 'Unknown') as jenis, COUNT(*)
FROM data_retribusi
WHERE %s AND "Jenis" IS NOT NULL AND TRIM("Jenis") != ''
GROUP BY "Jenis"
ORDER BY COUNT(*) DESC
LIMIT 10`, whereClause)
rows, err := dbConn.QueryContext(ctx, jenisQuery, args...)
if err != nil {
errChan <- fmt.Errorf("jenis query failed: %w", err)
return
}
defer rows.Close()
mu.Lock()
for rows.Next() {
var jenis string
var count int
if err := rows.Scan(&jenis, &count); err != nil {
mu.Unlock()
errChan <- fmt.Errorf("jenis scan failed: %w", err)
return
}
aggregate.ByJenis[jenis] = count
}
mu.Unlock()
if err := rows.Err(); err != nil {
errChan <- fmt.Errorf("jenis iteration error: %w", err)
}
}()
// 4. 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 data_retribusi
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 data_retribusi
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
}
// Get total count dengan filter support
func (h *RetribusiHandler) getTotalCount(ctx context.Context, db *gorm.DB, filter retribusi.RetribusiFilter, total *int64) error {
query := db.Model(&retribusi.Retribusi{}).Where("status != ?", "deleted")
if filter.Status != nil {
query = query.Where("status = ?", *filter.Status)
}
if filter.Jenis != nil {
query = query.Where("\"Jenis\" ILIKE ?", "%"+*filter.Jenis+"%")
}
if filter.Dinas != nil {
query = query.Where("\"Dinas\" ILIKE ?", "%"+*filter.Dinas+"%")
}
if filter.KelompokObyek != nil {
query = query.Where("\"Kelompok_obyek\" ILIKE ?", "%"+*filter.KelompokObyek+"%")
}
if filter.Search != nil {
searchTerm := "%" + *filter.Search + "%"
query = query.Where("\"Jenis\" ILIKE ? OR \"Pelayanan\" ILIKE ? OR \"Dinas\" ILIKE ? OR \"Kode_tarif\" ILIKE ? OR \"Uraian_1\" ILIKE ? OR \"Uraian_2\" ILIKE ? OR \"Uraian_3\" ILIKE ?", searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm)
}
if filter.DateFrom != nil {
query = query.Where("date_created >= ?", *filter.DateFrom)
}
if filter.DateTo != nil {
query = query.Where("date_created <= ?", filter.DateTo.Add(24*time.Hour-time.Nanosecond))
}
if err := query.Count(total).Error; err != nil {
return fmt.Errorf("total count query failed: %w", err)
}
return nil
}
// Enhanced fetchRetribusis dengan filter support
func (h *RetribusiHandler) fetchRetribusis(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter, limit, offset int) ([]retribusi.Retribusi, error) {
whereClause, args := h.buildWhereClause(filter)
// Build the main query with pagination
query := fmt.Sprintf(`
SELECT
id, status, sort, user_created, date_created, user_updated, date_updated,
"Jenis", "Pelayanan", "Dinas", "Kelompok_obyek", "Kode_tarif",
"Tarif", "Satuan", "Tarif_overtime", "Satuan_overtime",
"Rekening_pokok", "Rekening_denda", "Uraian_1", "Uraian_2", "Uraian_3"
FROM data_retribusi
WHERE %s
ORDER BY date_created DESC NULLS LAST
LIMIT $%d OFFSET $%d`,
whereClause, len(args)+1, len(args)+2)
// Add pagination parameters
args = append(args, limit, offset)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch retribusis query failed: %w", err)
}
defer rows.Close()
// Pre-allocate slice dengan kapasitas yang tepat
retribusis := make([]retribusi.Retribusi, 0, limit)
for rows.Next() {
retribusi, err := h.scanRetribusi(rows)
if err != nil {
return nil, fmt.Errorf("scan retribusi failed: %w", err)
}
retribusis = append(retribusis, retribusi)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
logger.Info("Successfully fetched retribusis", map[string]interface{}{
"count": len(retribusis),
"limit": limit,
"offset": offset,
})
return retribusis, nil
}
// Calculate pagination metadata
func (h *RetribusiHandler) 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,
}
}
// validateRetribusiSubmission performs validation for duplicate entries and daily submission limits
func (h *RetribusiHandler) validateRetribusiSubmission(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) error {
// Import the validation utility
validator := validation.NewDuplicateValidator(dbConn)
// Use default retribusi configuration
config := validation.DefaultRetribusiConfig()
// Validate duplicate entries with active status for today
err := validator.ValidateDuplicate(ctx, config, "dummy_id")
if err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Validate once per day submission
err = validator.ValidateOncePerDay(ctx, "data_retribusi", "id", "date_created", "daily_limit")
if err != nil {
return fmt.Errorf("daily submission limit exceeded: %w", err)
}
return nil
}
// Example usage of the validation utility with custom configuration
func (h *RetribusiHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) error {
// Create validator instance
validator := validation.NewDuplicateValidator(dbConn)
// Use custom configuration
config := validation.ValidationConfig{
TableName: "data_retribusi",
IDColumn: "id",
StatusColumn: "status",
DateColumn: "date_created",
ActiveStatuses: []string{"active", "draft"},
AdditionalFields: map[string]interface{}{
"jenis": req.Jenis,
"dinas": req.Dinas,
},
}
// Validate with custom fields
fields := map[string]interface{}{
"jenis": *req.Jenis,
"dinas": *req.Dinas,
}
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
if err != nil {
return fmt.Errorf("custom validation failed: %w", err)
}
return nil
}
// GetLastSubmissionTime example
func (h *RetribusiHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
validator := validation.NewDuplicateValidator(dbConn)
return validator.GetLastSubmissionTime(ctx, "data_retribusi", "id", "date_created", identifier)
}