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