package handlers import ( "api-service/internal/config" "api-service/internal/database" models "api-service/internal/models" registrasi "api-service/internal/models/registrasi" "api-service/internal/utils/validation" "context" "database/sql" "fmt" "log" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" ) var ( registrationCounterDB database.Service registrationCounterOnce sync.Once registrationCounterValidate *validator.Validate ) // Initialize the database connection and validator func init() { registrationCounterOnce.Do(func() { registrationCounterDB = database.New(config.LoadConfig()) registrationCounterValidate = validator.New() // Jika ada validasi khusus untuk status, daftarkan di sini // registrationCounterValidate.RegisterValidation("registration_counter_status", validateRegistrationCounterStatus) if registrationCounterDB == nil { log.Fatal("Failed to initialize database connection for registration counter") } }) } // RegistrationCounterHandler handles registration counter services type RegistrationCounterHandler struct { db database.Service } // NewRegistrationCounterHandler creates a new RegistrationCounterHandler func NewRegistrationCounterHandler() *RegistrationCounterHandler { return &RegistrationCounterHandler{ db: registrationCounterDB, } } // GetRegistrationCounters godoc // @Summary Get registration counters with pagination and optional aggregation // @Description Returns a paginated list of registration counters with optional summary statistics // @Tags RegistrationCounter // @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 active query bool false "Filter by active status" // @Param search query string false "Search in name or code" // @Success 200 {object} registrasi.MsRegistrationCounterGetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter [get] func (h *RegistrationCounterHandler) GetRegistrationCounters(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("default") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } // Create context with timeout ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) defer cancel() // Execute concurrent operations var ( items []registrasi.MsRegistrationCounter 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.fetchRegistrationCounters(ctx, dbConn, filter, limit, offset) mu.Lock() if err != nil { errChan <- fmt.Errorf("failed to fetch data: %w", err) } else { items = result } mu.Unlock() }() // Fetch aggregation data if requested if includeAggregation { wg.Add(1) go func() { defer wg.Done() result, err := h.getAggregateData(ctx, dbConn, filter) mu.Lock() if err != nil { errChan <- fmt.Errorf("failed to get aggregate data: %w", err) } else { aggregateData = result } mu.Unlock() }() } // Wait for all goroutines wg.Wait() close(errChan) // Check for errors for err := range errChan { if err != nil { h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError) return } } // Build response meta := h.calculateMeta(limit, offset, total) response := registrasi.MsRegistrationCounterGetResponse{ Message: "Data registration counter berhasil diambil", Data: items, Meta: meta, } if includeAggregation && aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) } // GetRegistrationCounterByID godoc // @Summary Get Registration Counter by ID // @Description Returns a single registration counter by its ID // @Tags RegistrationCounter // @Accept json // @Produce json // @Param id path int true "Registration Counter ID" // @Success 200 {object} registrasi.MsRegistrationCounterGetByIDResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "Registration Counter not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter/{id} [get] func (h *RegistrationCounterHandler) GetRegistrationCounterByID(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) return } dbConn, err := h.db.GetDB("default") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) defer cancel() item, err := h.getRegistrationCounterByID(ctx, dbConn, id) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Registration Counter not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to get registration counter", err, http.StatusInternalServerError) } return } response := registrasi.MsRegistrationCounterGetByIDResponse{ Message: "Detail registration counter berhasil diambil", Data: item, } c.JSON(http.StatusOK, response) } // CreateRegistrationCounter godoc // @Summary Create a new registration counter // @Description Creates a new registration counter record // @Tags RegistrationCounter // @Accept json // @Produce json // @Param request body registrasi.MsRegistrationCounterCreateRequest true "Registration Counter creation request" // @Success 201 {object} registrasi.MsRegistrationCounterCreateResponse "Registration Counter created successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter [post] func (h *RegistrationCounterHandler) CreateRegistrationCounter(c *gin.Context) { var req registrasi.MsRegistrationCounterCreateRequest if err := c.ShouldBindJSON(&req); err != nil { h.respondError(c, "Invalid request body", err, http.StatusBadRequest) return } // Validate request if err := registrationCounterValidate.Struct(&req); err != nil { h.respondError(c, "Validation failed", err, http.StatusBadRequest) return } dbConn, err := h.db.GetDB("default") 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 entries if err := h.validateRegistrationCounterSubmission(ctx, dbConn, &req); err != nil { h.respondError(c, "Validation failed", err, http.StatusBadRequest) return } item, err := h.createRegistrationCounter(ctx, dbConn, &req) if err != nil { h.logAndRespondError(c, "Failed to create registration counter", err, http.StatusInternalServerError) return } response := registrasi.MsRegistrationCounterCreateResponse{ Message: "Registration counter berhasil dibuat", Data: item, } c.JSON(http.StatusCreated, response) } // UpdateRegistrationCounter godoc // @Summary Update an existing registration counter // @Description Updates an existing registration counter record // @Tags RegistrationCounter // @Accept json // @Produce json // @Param id path int true "Registration Counter ID" // @Param request body registrasi.MsRegistrationCounterUpdateRequest true "Registration Counter update request" // @Success 200 {object} registrasi.MsRegistrationCounterUpdateResponse "Registration Counter updated successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 404 {object} models.ErrorResponse "Registration Counter not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter/{id} [put] func (h *RegistrationCounterHandler) UpdateRegistrationCounter(c *gin.Context) { id := c.Param("id") // Validate ID is integer if _, err := strconv.Atoi(id); err != nil { h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) return } var req registrasi.MsRegistrationCounterUpdateRequest if err := c.ShouldBindJSON(&req); err != nil { h.respondError(c, "Invalid request body", err, http.StatusBadRequest) return } // Set ID from path parameter idInt, _ := strconv.Atoi(id) req.ID = idInt // Validate request if err := registrationCounterValidate.Struct(&req); err != nil { h.respondError(c, "Validation failed", err, http.StatusBadRequest) return } dbConn, err := h.db.GetDB("default") if err != nil { h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError) return } ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second) defer cancel() item, err := h.updateRegistrationCounter(ctx, dbConn, &req) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Registration Counter not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to update registration counter", err, http.StatusInternalServerError) } return } response := registrasi.MsRegistrationCounterUpdateResponse{ Message: "Registration counter berhasil diperbarui", Data: item, } c.JSON(http.StatusOK, response) } // DeleteRegistrationCounter godoc // @Summary Soft delete a registration counter // @Description Soft deletes a registration counter by setting active to false // @Tags RegistrationCounter // @Accept json // @Produce json // @Param id path int true "Registration Counter ID" // @Success 200 {object} registrasi.MsRegistrationCounterDeleteResponse "Registration Counter deleted successfully" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "Registration Counter not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter/{id} [delete] func (h *RegistrationCounterHandler) DeleteRegistrationCounter(c *gin.Context) { idStr := c.Param("id") id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { h.respondError(c, "Invalid ID format", err, http.StatusBadRequest) return } dbConn, err := h.db.GetDB("default") 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.deleteRegistrationCounter(ctx, dbConn, id) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Registration Counter not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to delete registration counter", err, http.StatusInternalServerError) } return } response := registrasi.MsRegistrationCounterDeleteResponse{ Message: "Registration counter berhasil dihapus (diset tidak aktif)", ID: idStr, } c.JSON(http.StatusOK, response) } // GetRegistrationCounterStats godoc // @Summary Get registration counter statistics // @Description Returns comprehensive statistics about registration counter data // @Tags RegistrationCounter // @Accept json // @Produce json // @Param active query bool false "Filter statistics by active status" // @Success 200 {object} models.AggregateData "Statistics data" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /registration-counter/stats [get] func (h *RegistrationCounterHandler) GetRegistrationCounterStats(c *gin.Context) { dbConn, err := h.db.GetDB("default") 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 registration counter berhasil diambil", "data": aggregateData, }) } // Database operations // func (h *RegistrationCounterHandler) getRegistrationCounterByID(ctx context.Context, dbConn *sql.DB, id int64) (*registrasi.MsRegistrationCounter, error) { // query := `SELECT id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location // FROM master.ms_registration_counter WHERE id = $1` // row := dbConn.QueryRowContext(ctx, query, id) // var item registrasi.MsRegistrationCounter // err := row.Scan( // &item.ID, // &item.Name, // &item.Code, // &item.Icon, // &item.Quota, // &item.Active, // &item.FKRefHealtcareTypeID, // &item.FKRefServiceTypeID, // &item.FKSdLocationID, // &item.DsSdLocation, // ) // if err != nil { // return nil, err // } // return &item, nil // } func (h *RegistrationCounterHandler) getRegistrationCounterByID(ctx context.Context, dbConn *sql.DB, id int64) (*registrasi.MsRegistrationCounterDetail, error) { query := ` SELECT rc.id, rc.name, rc.code, rc.icon, rc.quota, rc.active, ht.name as healthcaretype, st.name as servicetype FROM master.ms_registration_counter rc LEFT JOIN reference.ref_healthcare_type ht ON rc.fk_ref_healtcare_type_id = ht.id LEFT JOIN reference.ref_service_type st ON rc.fk_ref_service_type_id = st.id WHERE rc.id = $1` row := dbConn.QueryRowContext(ctx, query, id) var item registrasi.MsRegistrationCounterDetail err := row.Scan( &item.ID, &item.Name, &item.Code, &item.Icon, &item.Quota, &item.Active, &item.HealthCareType, &item.ServiceType, ) if err != nil { return nil, err } return &item, nil } func (h *RegistrationCounterHandler) createRegistrationCounter(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterCreateRequest) (*registrasi.MsRegistrationCounter, error) { query := `INSERT INTO master.ms_registration_counter (name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location` row := dbConn.QueryRowContext( ctx, query, req.Name, req.Code, req.Icon, req.Quota, req.Active, req.FKRefHealtcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation, ) var item registrasi.MsRegistrationCounter err := row.Scan( &item.ID, &item.Name, &item.Code, &item.Icon, &item.Quota, &item.Active, &item.FKRefHealtcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation, ) if err != nil { return nil, fmt.Errorf("failed to create registration counter: %w", err) } return &item, nil } func (h *RegistrationCounterHandler) updateRegistrationCounter(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterUpdateRequest) (*registrasi.MsRegistrationCounter, error) { query := `UPDATE master.ms_registration_counter SET name = $2, code = $3, icon = $4, quota = $5, active = $6, fk_ref_healtcare_type_id = $7, fk_ref_service_type_id = $8, fk_sd_location_id = $9, ds_sd_location = $10 WHERE id = $1 RETURNING id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location` row := dbConn.QueryRowContext( ctx, query, req.ID, req.Name, req.Code, req.Icon, req.Quota, req.Active, req.FKRefHealtcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation, ) var item registrasi.MsRegistrationCounter err := row.Scan( &item.ID, &item.Name, &item.Code, &item.Icon, &item.Quota, &item.Active, &item.FKRefHealtcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation, ) if err != nil { return nil, fmt.Errorf("failed to update registration counter: %w", err) } return &item, nil } func (h *RegistrationCounterHandler) deleteRegistrationCounter(ctx context.Context, dbConn *sql.DB, id int64) error { query := `UPDATE master.ms_registration_counter SET active = false WHERE id = $1` result, err := dbConn.ExecContext(ctx, query, id) if err != nil { return fmt.Errorf("failed to delete registration counter: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get affected rows: %w", err) } if rowsAffected == 0 { return sql.ErrNoRows } return nil } func (h *RegistrationCounterHandler) fetchRegistrationCounters(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter, limit, offset int) ([]registrasi.MsRegistrationCounter, error) { whereClause, args := h.buildWhereClause(filter) query := fmt.Sprintf(`SELECT id, name, code, icon, quota, active, fk_ref_healtcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location FROM master.ms_registration_counter WHERE %s ORDER BY id LIMIT $%d OFFSET $%d`, whereClause, len(args)+1, len(args)+2) args = append(args, limit, offset) rows, err := dbConn.QueryContext(ctx, query, args...) if err != nil { return nil, fmt.Errorf("fetch registration counters query failed: %w", err) } defer rows.Close() items := make([]registrasi.MsRegistrationCounter, 0, limit) for rows.Next() { var item registrasi.MsRegistrationCounter err := rows.Scan( &item.ID, &item.Name, &item.Code, &item.Icon, &item.Quota, &item.Active, &item.FKRefHealtcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation, ) if err != nil { return nil, fmt.Errorf("scan registration counter failed: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("rows iteration error: %w", err) } log.Printf("Successfully fetched %d registration counters with filters applied", len(items)) return items, nil } func (h *RegistrationCounterHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter, total *int) error { whereClause, args := h.buildWhereClause(filter) countQuery := fmt.Sprintf("SELECT COUNT(*) FROM master.ms_registration_counter WHERE %s", whereClause) if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil { return fmt.Errorf("total count query failed: %w", err) } return nil } func (h *RegistrationCounterHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter registrasi.MsRegistrationCounterFilter) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } whereClause, args := h.buildWhereClause(filter) var wg sync.WaitGroup var mu sync.Mutex errChan := make(chan error, 2) // 1. Count by active status wg.Add(1) go func() { defer wg.Done() statusQuery := fmt.Sprintf("SELECT active, COUNT(*) FROM master.ms_registration_counter WHERE %s GROUP BY active ORDER BY active", whereClause) rows, err := dbConn.QueryContext(ctx, statusQuery, args...) if err != nil { errChan <- fmt.Errorf("status query failed: %w", err) return } defer rows.Close() mu.Lock() for rows.Next() { var active bool var count int if err := rows.Scan(&active, &count); err != nil { mu.Unlock() errChan <- fmt.Errorf("status scan failed: %w", err) return } if active { aggregate.ByStatus["active"] = count aggregate.TotalActive = count } else { aggregate.ByStatus["inactive"] = count aggregate.TotalInactive = count } } mu.Unlock() if err := rows.Err(); err != nil { errChan <- fmt.Errorf("status iteration error: %w", err) } }() // 2. Get total count wg.Add(1) go func() { defer wg.Done() // Last updated lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM master.ms_registration_counter WHERE %s AND date_updated IS NOT NULL", whereClause) var lastUpdated sql.NullTime if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil { errChan <- fmt.Errorf("last updated query failed: %w", err) return } // Today statistics today := time.Now().Format("2006-01-02") todayStatsQuery := fmt.Sprintf(` SELECT SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today, SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today FROM master.ms_registration_counter WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause) todayArgs := append(args, today) var createdToday, updatedToday int if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil { errChan <- fmt.Errorf("today stats query failed: %w", err) return } mu.Lock() if lastUpdated.Valid { aggregate.LastUpdated = &lastUpdated.Time } aggregate.CreatedToday = createdToday aggregate.UpdatedToday = updatedToday mu.Unlock() }() // Wait for all goroutines wg.Wait() close(errChan) // Check for errors for err := range errChan { if err != nil { return nil, err } } return aggregate, nil } // Helper methods func (h *RegistrationCounterHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) { log.Printf("[ERROR] %s: %v", message, err) h.respondError(c, message, err, statusCode) } func (h *RegistrationCounterHandler) 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(), }) } func (h *RegistrationCounterHandler) parsePaginationParams(c *gin.Context) (int, int, error) { limit := 10 offset := 0 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 || parsedLimit > 100 { return 0, 0, fmt.Errorf("limit must be between 1 and 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 } return limit, offset, nil } func (h *RegistrationCounterHandler) parseFilterParams(c *gin.Context) registrasi.MsRegistrationCounterFilter { filter := registrasi.MsRegistrationCounterFilter{} if status := c.Query("active"); status != "" { if models.IsValidStatus(status) { filter.Status = &status } } if search := c.Query("search"); search != "" { filter.Search = &search } return filter } func (h *RegistrationCounterHandler) buildWhereClause(filter registrasi.MsRegistrationCounterFilter) (string, []interface{}) { conditions := []string{"1=1"} args := []interface{}{} paramCount := 1 if filter.Status != nil { conditions = append(conditions, fmt.Sprintf("active = $%d", paramCount)) args = append(args, *filter.Status) paramCount++ } if filter.Search != nil { searchCondition := fmt.Sprintf("(name ILIKE $%d OR code ILIKE $%d)", paramCount, paramCount) conditions = append(conditions, searchCondition) searchTerm := "%" + *filter.Search + "%" args = append(args, searchTerm) paramCount++ } return strings.Join(conditions, " AND "), args } func (h *RegistrationCounterHandler) calculateMeta(limit, offset, total int) models.MetaResponse { totalPages := 0 currentPage := 1 if limit > 0 { totalPages = (total + limit - 1) / limit currentPage = (offset / limit) + 1 } return models.MetaResponse{ Limit: limit, Offset: offset, Total: total, TotalPages: totalPages, CurrentPage: currentPage, HasNext: offset+limit < total, HasPrev: offset > 0, } } func (h *RegistrationCounterHandler) validateRegistrationCounterSubmission(ctx context.Context, dbConn *sql.DB, req *registrasi.MsRegistrationCounterCreateRequest) error { validator := validation.NewDuplicateValidator(dbConn) config := validation.ValidationConfig{ TableName: "master.ms_registration_counter", IDColumn: "id", AdditionalFields: map[string]interface{}{ "name": req.Name, "code": req.Code, }, } fields := map[string]interface{}{ "name": req.Name, "code": req.Code, } err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields) if err != nil { return fmt.Errorf("validation failed: %w", err) } return nil }