package handlers import ( "api-service/internal/config" "api-service/internal/database" models "api-service/internal/models/order" "context" "database/sql" "fmt" "log" "net/http" "strconv" "strings" "sync" "time" "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("order_status", validateOrderStatus) if db == nil { log.Fatal("Failed to initialize database connection") } }) } // Custom validation for order status func validateOrderStatus(fl validator.FieldLevel) bool { return models.IsValidStatus(fl.Field().String()) } // OrderHandler handles order services type OrderHandler struct { db database.Service } // NewOrderHandler creates a new OrderHandler func NewOrderHandler() *OrderHandler { return &OrderHandler{ db: db, } } // GetOrder godoc // @Summary Get order with pagination and optional aggregation // @Description Returns a paginated list of orders with optional summary statistics // @Tags order // @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 search query string false "Search in multiple fields" // @Success 200 {object} models.OrderGetResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Bad request" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/orders [get] func (h *OrderHandler) GetOrder(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("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 ( items []models.Order 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 - FIXED: Proper method name wg.Add(1) go func() { defer wg.Done() result, err := h.fetchOrders(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 := models.OrderGetResponse{ Message: "Data order berhasil diambil", Data: items, Meta: meta, } if includeAggregation && aggregateData != nil { response.Summary = aggregateData } c.JSON(http.StatusOK, response) } // GetOrderByID godoc // @Summary Get Order by ID // @Description Returns a single order by ID // @Tags order // @Accept json // @Produce json // @Param id path string true "Order ID (UUID)" // @Success 200 {object} models.OrderGetByIDResponse "Success response" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "Order not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/order/{id} [get] func (h *OrderHandler) GetOrderByID(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("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() item, err := h.getOrderByID(ctx, dbConn, id) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Order not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to get order", err, http.StatusInternalServerError) } return } response := models.OrderGetByIDResponse{ Message: "Order details retrieved successfully", Data: item, } c.JSON(http.StatusOK, response) } // CreateOrder godoc // @Summary Create order // @Description Creates a new order record // @Tags order // @Accept json // @Produce json // @Param request body models.OrderCreateRequest true "Order creation request" // @Success 201 {object} models.OrderCreateResponse "Order created successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/orders [post] func (h *OrderHandler) CreateOrder(c *gin.Context) { var req models.OrderCreateRequest 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("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() item, err := h.createOrder(ctx, dbConn, &req) if err != nil { h.logAndRespondError(c, "Failed to create order", err, http.StatusInternalServerError) return } response := models.OrderCreateResponse{ Message: "Order berhasil dibuat", Data: item, } c.JSON(http.StatusCreated, response) } // UpdateOrder godoc // @Summary Update order // @Description Updates an existing order record // @Tags order // @Accept json // @Produce json // @Param id path string true "Order ID (UUID)" // @Param request body models.OrderUpdateRequest true "Order update request" // @Success 200 {object} models.OrderUpdateResponse "Order updated successfully" // @Failure 400 {object} models.ErrorResponse "Bad request or validation error" // @Failure 404 {object} models.ErrorResponse "Order not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/order/{id} [put] func (h *OrderHandler) UpdateOrder(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 models.OrderUpdateRequest 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("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() item, err := h.updateOrder(ctx, dbConn, &req) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Order not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to update order", err, http.StatusInternalServerError) } return } response := models.OrderUpdateResponse{ Message: "Order berhasil diperbarui", Data: item, } c.JSON(http.StatusOK, response) } // DeleteOrder godoc // @Summary Delete order // @Description Soft deletes a order by setting status to 'deleted' // @Tags order // @Accept json // @Produce json // @Param id path string true "Order ID (UUID)" // @Success 200 {object} models.OrderDeleteResponse "Order deleted successfully" // @Failure 400 {object} models.ErrorResponse "Invalid ID format" // @Failure 404 {object} models.ErrorResponse "Order not found" // @Failure 500 {object} models.ErrorResponse "Internal server error" // @Router /api/v1/order/{id} [delete] func (h *OrderHandler) DeleteOrder(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("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.deleteOrder(ctx, dbConn, id) if err != nil { if err == sql.ErrNoRows { h.respondError(c, "Order not found", err, http.StatusNotFound) } else { h.logAndRespondError(c, "Failed to delete order", err, http.StatusInternalServerError) } return } response := models.OrderDeleteResponse{ Message: "Order berhasil dihapus", ID: id, } c.JSON(http.StatusOK, response) } // GetOrderStats godoc // @Summary Get order statistics // @Description Returns comprehensive statistics about order data // @Tags order // @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/orders/stats [get] func (h *OrderHandler) GetOrderStats(c *gin.Context) { dbConn, err := h.db.GetDB("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 order berhasil diambil", "data": aggregateData, }) } // Database operations func (h *OrderHandler) getOrderByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Order, error) { query := "SELECT id, status, date_created, date_updated, name FROM data_order WHERE id = $1 AND status != 'deleted'" row := dbConn.QueryRowContext(ctx, query, id) var item models.Order err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) if err != nil { return nil, err } return &item, nil } func (h *OrderHandler) createOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderCreateRequest) (*models.Order, error) { id := uuid.New().String() now := time.Now() query := "INSERT INTO data_order (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name" row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name) var item models.Order err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) if err != nil { return nil, fmt.Errorf("failed to create order: %w", err) } return &item, nil } func (h *OrderHandler) updateOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderUpdateRequest) (*models.Order, error) { now := time.Now() query := "UPDATE data_order SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name" row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name) var item models.Order err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) if err != nil { return nil, fmt.Errorf("failed to update order: %w", err) } return &item, nil } func (h *OrderHandler) deleteOrder(ctx context.Context, dbConn *sql.DB, id string) error { now := time.Now() query := "UPDATE data_order SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'" result, err := dbConn.ExecContext(ctx, query, id, now) if err != nil { return fmt.Errorf("failed to delete order: %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 *OrderHandler) fetchOrders(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, limit, offset int) ([]models.Order, error) { whereClause, args := h.buildWhereClause(filter) query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_order WHERE %s ORDER BY date_created DESC NULLS LAST 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 orders query failed: %w", err) } defer rows.Close() items := make([]models.Order, 0, limit) for rows.Next() { var item models.Order err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name) if err != nil { return nil, fmt.Errorf("scan Order failed: %w", err) } items = append(items, item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("rows iteration error: %w", err) } return items, nil } func (h *OrderHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, total *int) error { whereClause, args := h.buildWhereClause(filter) countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_order 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 *OrderHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter) (*models.AggregateData, error) { aggregate := &models.AggregateData{ ByStatus: make(map[string]int), } whereClause, args := h.buildWhereClause(filter) statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_order WHERE %s GROUP BY status ORDER BY status", whereClause) rows, err := dbConn.QueryContext(ctx, statusQuery, args...) if err != nil { return nil, fmt.Errorf("status query failed: %w", err) } defer rows.Close() for rows.Next() { var status string var count int if err := rows.Scan(&status, &count); err != nil { return nil, fmt.Errorf("status scan failed: %w", err) } aggregate.ByStatus[status] = count switch status { case "active": aggregate.TotalActive = count case "draft": aggregate.TotalDraft = count case "inactive": aggregate.TotalInactive = count } } return aggregate, nil } // Helper methods func (h *OrderHandler) 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 *OrderHandler) 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 *OrderHandler) 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 } return limit, offset, nil } func (h *OrderHandler) parseFilterParams(c *gin.Context) models.OrderFilter { filter := models.OrderFilter{} if status := c.Query("status"); status != "" { if models.IsValidStatus(status) { filter.Status = &status } } 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 } func (h *OrderHandler) buildWhereClause(filter models.OrderFilter) (string, []interface{}) { conditions := []string{"status != 'deleted'"} args := []interface{}{} paramCount := 1 if filter.Status != nil { conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount)) args = append(args, *filter.Status) paramCount++ } if filter.Search != nil { searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount) conditions = append(conditions, searchCondition) searchTerm := "%" + *filter.Search + "%" args = append(args, searchTerm) paramCount++ } if filter.DateFrom != nil { conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount)) args = append(args, *filter.DateFrom) paramCount++ } if filter.DateTo != nil { conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount)) args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond)) paramCount++ } return strings.Join(conditions, " AND "), args } func (h *OrderHandler) 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, } }