1612 lines
46 KiB
Plaintext
1612 lines
46 KiB
Plaintext
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/Masterminds/squirrel" // Menambahkan library query builder
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
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
|
|
builder squirrel.StatementBuilderType // Menambahkan query builder
|
|
}
|
|
|
|
// NewRetribusiHandler creates a new RetribusiHandler
|
|
func NewRetribusiHandler() *RetribusiHandler {
|
|
return &RetribusiHandler{
|
|
db: db,
|
|
builder: squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar), // Menggunakan placeholder PostgreSQL
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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 concurrent operations
|
|
var (
|
|
retribusis []retribusi.Retribusi
|
|
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.fetchRetribusis(ctx, dbConn, 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, 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 := 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
|
|
}
|
|
|
|
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.getRetribusiByID(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 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 query builder
|
|
func (h *RetribusiHandler) getRetribusiByID(ctx context.Context, dbConn *sql.DB, id string) (*retribusi.Retribusi, error) {
|
|
// Menggunakan query builder untuk membuat query SELECT
|
|
query := h.builder.
|
|
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(squirrel.Eq{"id": id}).
|
|
Where(squirrel.NotEq{"status": "deleted"})
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
row := dbConn.QueryRowContext(ctx, sql, args...)
|
|
|
|
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, err
|
|
}
|
|
|
|
return &retribusi, nil
|
|
}
|
|
|
|
// Create retribusi using query builder
|
|
func (h *RetribusiHandler) createRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiCreateRequest) (*retribusi.Retribusi, error) {
|
|
id := uuid.New().String()
|
|
now := time.Now()
|
|
|
|
// Menggunakan query builder untuk membuat query INSERT
|
|
query := h.builder.
|
|
Insert("data_retribusi").
|
|
Columns(
|
|
"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(
|
|
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,
|
|
).
|
|
Suffix("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")
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
row := dbConn.QueryRowContext(ctx, sql, args...)
|
|
|
|
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 using query builder
|
|
func (h *RetribusiHandler) updateRetribusi(ctx context.Context, dbConn *sql.DB, req *retribusi.RetribusiUpdateRequest) (*retribusi.Retribusi, error) {
|
|
now := time.Now()
|
|
|
|
// Menggunakan query builder untuk membuat query UPDATE
|
|
query := h.builder.
|
|
Update("data_retribusi").
|
|
Set("status", req.Status).
|
|
Set("date_updated", now).
|
|
Set("Jenis", req.Jenis).
|
|
Set("Pelayanan", req.Pelayanan).
|
|
Set("Dinas", req.Dinas).
|
|
Set("Kelompok_obyek", req.KelompokObyek).
|
|
Set("Kode_tarif", req.KodeTarif).
|
|
Set("Tarif", req.Tarif).
|
|
Set("Satuan", req.Satuan).
|
|
Set("Tarif_overtime", req.TarifOvertime).
|
|
Set("Satuan_overtime", req.SatuanOvertime).
|
|
Set("Rekening_pokok", req.RekeningPokok).
|
|
Set("Rekening_denda", req.RekeningDenda).
|
|
Set("Uraian_1", req.Uraian1).
|
|
Set("Uraian_2", req.Uraian2).
|
|
Set("Uraian_3", req.Uraian3).
|
|
Where(squirrel.Eq{"id": req.ID}).
|
|
Where(squirrel.NotEq{"status": "deleted"}).
|
|
Suffix("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")
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := query.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
row := dbConn.QueryRowContext(ctx, sql, args...)
|
|
|
|
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 using query builder
|
|
func (h *RetribusiHandler) deleteRetribusi(ctx context.Context, dbConn *sql.DB, id string) error {
|
|
now := time.Now()
|
|
|
|
// Menggunakan query builder untuk membuat query UPDATE
|
|
query := h.builder.
|
|
Update("data_retribusi").
|
|
Set("status", "deleted").
|
|
Set("date_updated", now).
|
|
Where(squirrel.Eq{"id": id}).
|
|
Where(squirrel.NotEq{"status": "deleted"})
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := query.ToSql()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
result, err := dbConn.ExecContext(ctx, sql, args...)
|
|
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 menggunakan query builder
|
|
func (h *RetribusiHandler) buildWhereClause(filter retribusi.RetribusiFilter) (string, []interface{}) {
|
|
// Menggunakan query builder untuk membuat WHERE clause
|
|
builder := h.builder.Select("count(*)").From("data_retribusi")
|
|
|
|
// Filter default untuk mengecualikan record yang dihapus
|
|
builder = builder.Where(squirrel.NotEq{"status": "deleted"})
|
|
|
|
// Menambahkan filter berdasarkan parameter
|
|
if filter.Status != nil {
|
|
builder = builder.Where(squirrel.Eq{"status": *filter.Status})
|
|
}
|
|
|
|
if filter.Jenis != nil {
|
|
builder = builder.Where(squirrel.Like{"Jenis": "%" + *filter.Jenis + "%"})
|
|
}
|
|
|
|
if filter.Dinas != nil {
|
|
builder = builder.Where(squirrel.Like{"Dinas": "%" + *filter.Dinas + "%"})
|
|
}
|
|
|
|
if filter.KelompokObyek != nil {
|
|
builder = builder.Where(squirrel.Like{"Kelompok_obyek": "%" + *filter.KelompokObyek + "%"})
|
|
}
|
|
|
|
if filter.Search != nil {
|
|
searchTerm := "%" + *filter.Search + "%"
|
|
builder = builder.Where(squirrel.Or{
|
|
squirrel.Like{"Jenis": searchTerm},
|
|
squirrel.Like{"Pelayanan": searchTerm},
|
|
squirrel.Like{"Dinas": searchTerm},
|
|
squirrel.Like{"Kode_tarif": searchTerm},
|
|
squirrel.Like{"Uraian_1": searchTerm},
|
|
squirrel.Like{"Uraian_2": searchTerm},
|
|
squirrel.Like{"Uraian_3": searchTerm},
|
|
})
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
builder = builder.Where(squirrel.GtOrEq{"date_created": *filter.DateFrom})
|
|
}
|
|
|
|
if filter.DateTo != nil {
|
|
endOfDay := filter.DateTo.Add(24*time.Hour - time.Nanosecond)
|
|
builder = builder.Where(squirrel.LtOrEq{"date_created": endOfDay})
|
|
}
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := builder.ToSql()
|
|
if err != nil {
|
|
return "", nil
|
|
}
|
|
|
|
// Menghapus prefix "SELECT count(*) FROM " dari SQL untuk mendapatkan WHERE clause saja
|
|
whereClause := strings.Replace(sql, "SELECT count(*) FROM data_retribusi ", "", 1)
|
|
|
|
return whereClause, 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 menggunakan query builder
|
|
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 base query dengan filter
|
|
baseBuilder := h.builder.Select("*").From("data_retribusi").Where(squirrel.NotEq{"status": "deleted"})
|
|
|
|
// Menambahkan filter berdasarkan parameter
|
|
if filter.Status != nil {
|
|
baseBuilder = baseBuilder.Where(squirrel.Eq{"status": *filter.Status})
|
|
}
|
|
|
|
if filter.Jenis != nil {
|
|
baseBuilder = baseBuilder.Where(squirrel.Like{"Jenis": "%" + *filter.Jenis + "%"})
|
|
}
|
|
|
|
if filter.Dinas != nil {
|
|
baseBuilder = baseBuilder.Where(squirrel.Like{"Dinas": "%" + *filter.Dinas + "%"})
|
|
}
|
|
|
|
if filter.KelompokObyek != nil {
|
|
baseBuilder = baseBuilder.Where(squirrel.Like{"Kelompok_obyek": "%" + *filter.KelompokObyek + "%"})
|
|
}
|
|
|
|
if filter.Search != nil {
|
|
searchTerm := "%" + *filter.Search + "%"
|
|
baseBuilder = baseBuilder.Where(squirrel.Or{
|
|
squirrel.Like{"Jenis": searchTerm},
|
|
squirrel.Like{"Pelayanan": searchTerm},
|
|
squirrel.Like{"Dinas": searchTerm},
|
|
squirrel.Like{"Kode_tarif": searchTerm},
|
|
squirrel.Like{"Uraian_1": searchTerm},
|
|
squirrel.Like{"Uraian_2": searchTerm},
|
|
squirrel.Like{"Uraian_3": searchTerm},
|
|
})
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
baseBuilder = baseBuilder.Where(squirrel.GtOrEq{"date_created": *filter.DateFrom})
|
|
}
|
|
|
|
if filter.DateTo != nil {
|
|
endOfDay := filter.DateTo.Add(24*time.Hour - time.Nanosecond)
|
|
baseBuilder = baseBuilder.Where(squirrel.LtOrEq{"date_created": endOfDay})
|
|
}
|
|
|
|
// 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()
|
|
|
|
// Build query untuk count by status
|
|
statusBuilder := baseBuilder.
|
|
Select("status", "COUNT(*)").
|
|
GroupBy("status").
|
|
OrderBy("status")
|
|
|
|
sql, args, err := statusBuilder.ToSql()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("status query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
rows, err := dbConn.QueryContext(ctx, sql, 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()
|
|
|
|
// Build query untuk count by dinas
|
|
dinasBuilder := baseBuilder.
|
|
Select("COALESCE(Dinas, 'Unknown') as dinas", "COUNT(*)").
|
|
Where(squirrel.NotEq{"Dinas": nil}).
|
|
Where(squirrel.NotEq{"Dinas": ""}).
|
|
GroupBy("Dinas").
|
|
OrderBy("COUNT(*) DESC").
|
|
Limit(10)
|
|
|
|
sql, args, err := dinasBuilder.ToSql()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("dinas query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
rows, err := dbConn.QueryContext(ctx, sql, 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()
|
|
|
|
// Build query untuk count by jenis
|
|
jenisBuilder := baseBuilder.
|
|
Select("COALESCE(Jenis, 'Unknown') as jenis", "COUNT(*)").
|
|
Where(squirrel.NotEq{"Jenis": nil}).
|
|
Where(squirrel.NotEq{"Jenis": ""}).
|
|
GroupBy("Jenis").
|
|
OrderBy("COUNT(*) DESC").
|
|
Limit(10)
|
|
|
|
sql, args, err := jenisBuilder.ToSql()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("jenis query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
rows, err := dbConn.QueryContext(ctx, sql, 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
|
|
lastUpdatedBuilder := baseBuilder.
|
|
Select("MAX(date_updated)").
|
|
Where(squirrel.NotEq{"date_updated": nil})
|
|
|
|
sql, args, err := lastUpdatedBuilder.ToSql()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
var lastUpdated sql.NullTime
|
|
if err := dbConn.QueryRowContext(ctx, sql, args...).Scan(&lastUpdated); err != nil {
|
|
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
// Today statistics
|
|
today := time.Now().Format("2006-01-02")
|
|
todayStatsBuilder := baseBuilder.
|
|
Select(
|
|
"SUM(CASE WHEN DATE(date_created) = ? THEN 1 ELSE 0 END) as created_today",
|
|
"SUM(CASE WHEN DATE(date_updated) = ? AND DATE(date_created) != ? THEN 1 ELSE 0 END) as updated_today",
|
|
).
|
|
Where(squirrel.GtOrEq{"date_created": today}).
|
|
Where(squirrel.LtOrEq{"date_created": today + " 23:59:59"})
|
|
|
|
sql, args, err = todayStatsBuilder.ToSql()
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
|
return
|
|
}
|
|
|
|
var createdToday, updatedToday int
|
|
if err := dbConn.QueryRowContext(ctx, sql, args...).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 menggunakan query builder
|
|
func (h *RetribusiHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter, total *int) error {
|
|
// Build query untuk count
|
|
builder := h.builder.Select("COUNT(*)").From("data_retribusi").Where(squirrel.NotEq{"status": "deleted"})
|
|
|
|
// Menambahkan filter berdasarkan parameter
|
|
if filter.Status != nil {
|
|
builder = builder.Where(squirrel.Eq{"status": *filter.Status})
|
|
}
|
|
|
|
if filter.Jenis != nil {
|
|
builder = builder.Where(squirrel.Like{"Jenis": "%" + *filter.Jenis + "%"})
|
|
}
|
|
|
|
if filter.Dinas != nil {
|
|
builder = builder.Where(squirrel.Like{"Dinas": "%" + *filter.Dinas + "%"})
|
|
}
|
|
|
|
if filter.KelompokObyek != nil {
|
|
builder = builder.Where(squirrel.Like{"Kelompok_obyek": "%" + *filter.KelompokObyek + "%"})
|
|
}
|
|
|
|
if filter.Search != nil {
|
|
searchTerm := "%" + *filter.Search + "%"
|
|
builder = builder.Where(squirrel.Or{
|
|
squirrel.Like{"Jenis": searchTerm},
|
|
squirrel.Like{"Pelayanan": searchTerm},
|
|
squirrel.Like{"Dinas": searchTerm},
|
|
squirrel.Like{"Kode_tarif": searchTerm},
|
|
squirrel.Like{"Uraian_1": searchTerm},
|
|
squirrel.Like{"Uraian_2": searchTerm},
|
|
squirrel.Like{"Uraian_3": searchTerm},
|
|
})
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
builder = builder.Where(squirrel.GtOrEq{"date_created": *filter.DateFrom})
|
|
}
|
|
|
|
if filter.DateTo != nil {
|
|
endOfDay := filter.DateTo.Add(24*time.Hour - time.Nanosecond)
|
|
builder = builder.Where(squirrel.LtOrEq{"date_created": endOfDay})
|
|
}
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := builder.ToSql()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
if err := dbConn.QueryRowContext(ctx, sql, args...).Scan(total); err != nil {
|
|
return fmt.Errorf("total count query failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Enhanced fetchRetribusis dengan filter support menggunakan query builder
|
|
func (h *RetribusiHandler) fetchRetribusis(ctx context.Context, dbConn *sql.DB, filter retribusi.RetribusiFilter, limit, offset int) ([]retribusi.Retribusi, error) {
|
|
// Build query untuk fetch data
|
|
builder := h.builder.
|
|
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(squirrel.NotEq{"status": "deleted"}).
|
|
OrderBy("date_created DESC NULLS LAST").
|
|
Limit(uint64(limit)).
|
|
Offset(uint64(offset))
|
|
|
|
// Menambahkan filter berdasarkan parameter
|
|
if filter.Status != nil {
|
|
builder = builder.Where(squirrel.Eq{"status": *filter.Status})
|
|
}
|
|
|
|
if filter.Jenis != nil {
|
|
builder = builder.Where(squirrel.Like{"Jenis": "%" + *filter.Jenis + "%"})
|
|
}
|
|
|
|
if filter.Dinas != nil {
|
|
builder = builder.Where(squirrel.Like{"Dinas": "%" + *filter.Dinas + "%"})
|
|
}
|
|
|
|
if filter.KelompokObyek != nil {
|
|
builder = builder.Where(squirrel.Like{"Kelompok_obyek": "%" + *filter.KelompokObyek + "%"})
|
|
}
|
|
|
|
if filter.Search != nil {
|
|
searchTerm := "%" + *filter.Search + "%"
|
|
builder = builder.Where(squirrel.Or{
|
|
squirrel.Like{"Jenis": searchTerm},
|
|
squirrel.Like{"Pelayanan": searchTerm},
|
|
squirrel.Like{"Dinas": searchTerm},
|
|
squirrel.Like{"Kode_tarif": searchTerm},
|
|
squirrel.Like{"Uraian_1": searchTerm},
|
|
squirrel.Like{"Uraian_2": searchTerm},
|
|
squirrel.Like{"Uraian_3": searchTerm},
|
|
})
|
|
}
|
|
|
|
if filter.DateFrom != nil {
|
|
builder = builder.Where(squirrel.GtOrEq{"date_created": *filter.DateFrom})
|
|
}
|
|
|
|
if filter.DateTo != nil {
|
|
endOfDay := filter.DateTo.Add(24*time.Hour - time.Nanosecond)
|
|
builder = builder.Where(squirrel.LtOrEq{"date_created": endOfDay})
|
|
}
|
|
|
|
// Mendapatkan SQL dan argumen
|
|
sql, args, err := builder.ToSql()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build query: %w", err)
|
|
}
|
|
|
|
// Eksekusi query
|
|
rows, err := dbConn.QueryContext(ctx, sql, 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)
|
|
}
|