package feedback import ( "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 Service logger logger.Logger userHandler *user.Handler customerHandler *customer.Handler } // NewHandler creates a new feedback handler func NewHandler( service Service, 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) Search(c echo.Context) error { form, err := response.Parse[SearchForm](c) if err != nil { return response.ValidationError(c, "Invalid request data", err.Error()) } pagination, err := response.NewPagination(c) if err != nil { return response.PaginationBadRequest(c, err) } feedbackList, err := h.service.Search(c.Request().Context(), *form, pagination) if err != nil { if response.IsListPaginationError(err) { return response.PaginationBadRequest(c, err) } return response.InternalServerError(c, "Failed to retrieve feedback list") } return response.SuccessWithMeta(c, feedbackList, feedbackList.Meta, "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) Get(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.Get(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) Delete(c echo.Context) error { id := c.Param("id") if id == "" { return response.BadRequest(c, "Feedback ID is required", "") } // Call service err := h.service.Delete(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) Toggle(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.Toggle(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 for the authenticated customer with optional filters. // @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) PublicList(c echo.Context) error { customerID, err := customer.GetCustomerIDFromContext(c) if err != nil { return response.BadRequest(c, "Customer ID required", "User must be associated with a customer") } form, err := response.Parse[SearchForm](c) if err != nil { return response.ValidationError(c, "Invalid request data", err.Error()) } form.Customer = &customerID pagination, err := response.NewPagination(c) if err != nil { return response.PaginationBadRequest(c, err) } result, err := h.service.Search(c.Request().Context(), *form, pagination) if err != nil { if response.IsListPaginationError(err) { return response.PaginationBadRequest(c, err) } return response.InternalServerError(c, "Failed to list feedback") } return response.SuccessWithMeta(c, result, result.Meta, "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) PublicGet(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.Get(c.Request().Context(), feedbackID) if err != nil { return response.BadRequest(c, "Failed to get feedback", "") } return response.Success(c, feedback, "Feedback retrieved successfully") } // PublicGetFeedbackByTenderID retrieves feedback by tender ID // @Summary Get feedback by tender ID // @Description Retrieve feedback details by tender ID. This endpoint provides basic feedback information for public users. // @Tags Feedback // @Accept json // @Produce json // @Param tender_id path string true "Tender 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 tender 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/tender/{tender_id} [get] func (h *Handler) PublicGetByTenderID(c echo.Context) error { tenderID := c.Param("tender_id") if tenderID == "" { return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing") } customerID, err := customer.GetCustomerIDFromContext(c) if err != nil { return response.BadRequest(c, "Customer ID required", "User must be associated with a customer") } feedback, err := h.service.GetByTenderIDForCustomer(c.Request().Context(), tenderID, customerID) if err != nil { return response.BadRequest(c, "Failed to get feedback", "") } return response.Success(c, feedback, "Feedback retrieved successfully") } // GetCustomerStats retrieves feedback statistics for the authenticated customer. // @Summary Get customer feedback statistics // @Description Retrieve feedback statistics (likes, dislikes, totals) for the authenticated customer only. // @Tags Feedback // @Accept json // @Produce json // @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association" // @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /api/v1/feedback/stats/customer [get] func (h *Handler) GetCustomerStats(c echo.Context) error { customerID, err := customer.GetCustomerIDFromContext(c) if err != nil { return response.BadRequest(c, "Customer ID required", "User must be associated with a customer") } stats, err := h.service.CustomerStats(c.Request().Context(), customerID) if err != nil { return response.InternalServerError(c, "Failed to retrieve customer stats") } return response.Success(c, stats, "Customer stats retrieved successfully") } // GetCompanyStats returns customer-scoped stats for backward compatibility with clients using /stats/company. // @Summary Get feedback statistics (customer-scoped, legacy path) // @Description Same as GET /feedback/stats/customer. Prefer /stats/customer for new integrations. // @Tags Feedback // @Accept json // @Produce json // @Success 200 {object} response.APIResponse{data=CustomerStatsResponse} "Customer feedback statistics retrieved successfully" // @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing customer association" // @Failure 401 {object} response.APIResponse{error=string,message=string} "Unauthorized - User not authenticated" // @Failure 500 {object} response.APIResponse{error=string,message=string} "Internal server error" // @Security BearerAuth // @Router /api/v1/feedback/stats/company [get] func (h *Handler) GetCompanyStats(c echo.Context) error { return h.GetCustomerStats(c) }