package feedback import ( "strconv" "tm/internal/customer" "tm/internal/user" "tm/pkg/logger" "tm/pkg/response" "github.com/asaskevich/govalidator" "github.com/labstack/echo/v4" ) // Handler handles HTTP requests for feedback operations type Handler struct { service FeedbackService logger logger.Logger userHandler *user.Handler customerHandler *customer.Handler } // NewFeedbackHandler creates a new feedback handler func NewFeedbackHandler( service FeedbackService, userHandler *user.Handler, customerHandler *customer.Handler, logger logger.Logger, ) *Handler { return &Handler{ service: service, logger: logger, userHandler: userHandler, customerHandler: customerHandler, } } // *************** ADMIN *************** // ListFeedback retrieves feedback with pagination and filtering // @Summary List feedback with filters // @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for administrative use and provides comprehensive filtering options. // @Tags Admin-Feedback // @Accept json // @Produce json // @Param tender_id query string false "Filter by tender ID (MongoDB ObjectID format)" // @Param customer_id query string false "Filter by customer ID (MongoDB ObjectID format)" // @Param company_id query string false "Filter by company ID (MongoDB ObjectID format)" // @Param feedback_type query string false "Filter by feedback type" Enums(like, dislike) // @Param date_from query int64 false "Filter by creation date from (Unix timestamp in seconds)" // @Param date_to query int64 false "Filter by creation date to (Unix timestamp in seconds)" // @Param limit query int false "Number of items per page (default: 20, max: 100)" minimum(1) maximum(100) // @Param offset query int false "Number of items to skip for pagination (default: 0)" minimum(0) // @Success 200 {object} response.APIResponse{data=FeedbackListResponse} "Feedback list retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid query parameters" // @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid filter criteria" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /admin/v1/feedback [get] func (h *Handler) ListFeedback(c echo.Context) error { // Parse query parameters limitStr := c.QueryParam("limit") offsetStr := c.QueryParam("offset") limit := 20 // default limit offset := 0 // default offset if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } // Build search criteria criteria := FeedbackSearchCriteria{ Limit: limit, Offset: offset, } // Parse optional filters if tenderID := c.QueryParam("tender_id"); tenderID != "" { criteria.TenderID = &tenderID } if customerID := c.QueryParam("customer_id"); customerID != "" { criteria.CustomerID = &customerID } if companyID := c.QueryParam("company_id"); companyID != "" { criteria.CompanyID = &companyID } if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" { ft := FeedbackType(feedbackType) criteria.FeedbackType = &ft } if dateFrom := c.QueryParam("date_from"); dateFrom != "" { if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil { criteria.DateFrom = &df } } if dateTo := c.QueryParam("date_to"); dateTo != "" { if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil { criteria.DateTo = &dt } } // Validate date range if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo { return response.ValidationError(c, "Invalid date range", "date_from must be before date_to") } // Get enhanced feedback list with related data feedbackList, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset) if err != nil { return response.InternalServerError(c, "Failed to retrieve feedback list") } return response.Success(c, feedbackList.Feedback, "feedback list retrieved successfully") } // GetFeedbackByID retrieves feedback by ID with loaded related data // @Summary Get feedback by ID // @Description Retrieve feedback details by its unique identifier with loaded tender, customer, and company information. This endpoint provides comprehensive feedback data including related entity details. // @Tags Admin-Feedback // @Accept json // @Produce json // @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24) // @Success 200 {object} response.APIResponse{data=FeedbackResponse} "feedback retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format" // @Failure 404 {object} response.APIResponse{error=string,message=string} "Feedback not found" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /admin/v1/feedback/{id} [get] func (h *Handler) GetFeedback(c echo.Context) error { feedbackID := c.Param("id") if feedbackID == "" { return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing") } feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID) if err != nil { if err == ErrFeedbackNotFound { return response.NotFound(c, "Feedback not found") } if err == ErrFeedbackDeleted { return response.NotFound(c, "Feedback has been deleted") } return response.InternalServerError(c, "Failed to retrieve feedback") } return response.Success(c, feedback, "feedback retrieved successfully") } // SoftDeleteFeedback marks feedback as deleted without removing it // @Summary Soft delete feedback // @Description Mark feedback as deleted without permanently removing it from the database. The feedback will be marked as deleted and hidden from normal queries but can be restored if needed. // @Tags Admin-Feedback // @Accept json // @Produce json // @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24) // @Success 200 {object} response.APIResponse{message=string} "Feedback soft deleted successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format" // @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated" // @Failure 403 {object} response.APIResponse{error=string,message=string} "Forbidden - User not authorized to delete this feedback" // @Failure 404 {object} response.APIResponse{error=string,message=string} "Not found - Feedback not found" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /admin/v1/feedback/{id} [delete] func (h *Handler) DeleteFeedback(c echo.Context) error { id := c.Param("id") if id == "" { return response.BadRequest(c, "Feedback ID is required", "") } // Call service err := h.service.DeleteFeedback(c.Request().Context(), id) if err != nil { switch err { case ErrFeedbackNotFound: return response.BadRequest(c, "Feedback not found", "") case ErrUnauthorizedAccess: return response.Forbidden(c, "You are not authorized to delete this feedback") default: return response.InternalServerError(c, "Failed to soft delete feedback") } } return response.Success(c, nil, "Feedback soft deleted successfully") } // *************** PUBLIC *************** // ToggleFeedback toggles between like and dislike for a tender // @Summary Toggle feedback for a tender // @Description Toggle between like and dislike for a tender, or create a like if no feedback exists. This endpoint allows users to express their preference for tenders and automatically handles the toggle logic. // @Tags Feedback // @Accept json // @Produce json // @Param feedback body ToggleFeedbackForm true "Feedback toggle request containing tender ID" // @Success 200 {object} response.APIResponse{data=FeedbackResponse} "Feedback toggled successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid input data or missing customer/company association" // @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated" // @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid request data format" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /api/v1/feedback [post] func (h *Handler) ToggleFeedback(c echo.Context) error { form, err := response.Parse[ToggleFeedbackForm](c) if err != nil { return response.ValidationError(c, "Invalid request data", err.Error()) } // Validate form using govalidator if _, err := govalidator.ValidateStruct(form); err != nil { return response.ValidationError(c, "Validation failed", err.Error()) } // Get customer ID from context customerID, err := customer.GetCustomerIDFromContext(c) if err != nil { return response.BadRequest(c, "Customer ID required", "User must be associated with a customer") } // Get company ID from context companyID, err := user.GetCompanyIDFromContext(c) if err != nil { return response.BadRequest(c, "Company ID required", "User must be associated with a company") } // Call service result, err := h.service.ToggleFeedback(c.Request().Context(), *form, &customerID, &companyID) if err != nil { return response.InternalServerError(c, "Failed to toggle feedback") } return response.Success(c, result, "Feedback toggled successfully") } // ListFeedback retrieves feedback with pagination and filtering // @Summary List feedback with filters // @Description Retrieve paginated list of feedback with optional filtering by various criteria. This endpoint is for public use and filters results based on the authenticated user's company. // @Tags Feedback // @Accept json // @Produce json // @Param tender_id query string false "Filter by tender ID (MongoDB ObjectID format)" // @Param customer_id query string false "Filter by customer ID (MongoDB ObjectID format)" // @Param company_id query string false "Filter by company ID (MongoDB ObjectID format)" // @Param feedback_type query string false "Filter by feedback type" Enums(like, dislike) // @Param date_from query int64 false "Filter by creation date from (Unix timestamp in seconds)" minimum(0) // @Param date_to query int64 false "Filter by creation date to (Unix timestamp in seconds)" minimum(0) // @Param limit query int false "Number of items per page (default: 20, max: 100)" minimum(1) maximum(100) // @Param offset query int false "Number of items to skip for pagination (default: 0)" minimum(0) // @Success 200 {object} response.APIResponse{data=FeedbackListResponse} "Feedback list retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid query parameters or missing company association" // @Failure 422 {object} response.APIResponse{error=string,message=string} "Validation error - Invalid filter criteria" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /api/v1/feedback [get] func (h *Handler) PublicListFeedback(c echo.Context) error { // Parse query parameters limitStr := c.QueryParam("limit") offsetStr := c.QueryParam("offset") limit := 20 offset := 0 if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } companyID, err := user.GetCompanyIDFromContext(c) if err != nil { return response.BadRequest(c, "Company ID required", "User must be associated with a company") } // Build search criteria criteria := FeedbackSearchCriteria{ Limit: limit, Offset: offset, CompanyID: &companyID, } if feedbackType := c.QueryParam("feedback_type"); feedbackType != "" { ft := FeedbackType(feedbackType) if ft != FeedbackTypeLike && ft != FeedbackTypeDislike { return response.BadRequest(c, "Invalid feedback type", "Must be 'like' or 'dislike'") } criteria.FeedbackType = &ft } if dateFrom := c.QueryParam("date_from"); dateFrom != "" { if df, err := strconv.ParseInt(dateFrom, 10, 64); err == nil && df >= 0 { criteria.DateFrom = &df } } if dateTo := c.QueryParam("date_to"); dateTo != "" { if dt, err := strconv.ParseInt(dateTo, 10, 64); err == nil && dt >= 0 { criteria.DateTo = &dt } } // Validate date range if criteria.DateFrom != nil && criteria.DateTo != nil && *criteria.DateFrom >= *criteria.DateTo { return response.ValidationError(c, "Invalid date range", "date_from must be before date_to") } // Call service result, err := h.service.ListFeedback(c.Request().Context(), criteria, limit, offset) if err != nil { return response.InternalServerError(c, "Failed to list feedback") } return response.Success(c, result, "Feedback list retrieved successfully") } // PublicGetFeedback retrieves feedback by ID // @Summary Get feedback by ID // @Description Retrieve feedback details by its unique identifier. This endpoint provides basic feedback information for public users. // @Tags Feedback // @Accept json // @Produce json // @Param id path string true "Feedback ID (MongoDB ObjectID format)" minlength(24) maxlength(24) // @Success 200 {object} response.APIResponse{data=FeedbackResponse} "Feedback retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Invalid feedback ID format" // @Failure 404 {object} response.APIResponse{error=string,message=string} "Feedback not found" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /api/v1/feedback/{id} [get] func (h *Handler) PublicGetFeedback(c echo.Context) error { feedbackID := c.Param("id") if feedbackID == "" { return response.BadRequest(c, "Feedback ID is required", "Feedback ID parameter is missing") } feedback, err := h.service.GetFeedback(c.Request().Context(), feedbackID) if err != nil { return response.BadRequest(c, "Failed to get feedback", "") } return response.Success(c, feedback, "Feedback retrieved successfully") }