Files
tm_back/internal/feedback/handler.go
T
n.nakhostin 0e14a11daf Add Company Feedback Statistics Endpoint and Update API Documentation
- Introduced a new endpoint to retrieve comprehensive feedback statistics for the authenticated user's company, including total likes, dislikes, and percentage calculations.
- Implemented the GetCompanyFeedbackStats method in the feedback handler to handle requests, ensuring proper authentication and error handling.
- Updated Swagger JSON, YAML, and Go documentation to accurately reflect the new endpoint, including detailed descriptions, parameters, and response formats for the CompanyFeedbackStatsResponse.
- Enhanced the feedback repository and service layers to support the new statistics functionality, improving the overall data handling capabilities of the tender management system.
- These changes enhance the usability and functionality of the API, providing valuable insights into company feedback statistics.
2025-08-25 13:39:35 +03:30

407 lines
17 KiB
Go

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")
}
// 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) PublicGetFeedbackByTenderID(c echo.Context) error {
tenderID := c.Param("tender_id")
if tenderID == "" {
return response.BadRequest(c, "Tender ID is required", "Tender ID parameter is missing")
}
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
}
feedback, err := h.service.GetFeedbackByTenderID(c.Request().Context(), tenderID, companyID)
if err != nil {
return response.BadRequest(c, "Failed to get feedback", "")
}
return response.Success(c, feedback, "Feedback retrieved successfully")
}
// GetCompanyFeedbackStats retrieves feedback statistics for the authenticated user's company
// @Summary Get company feedback statistics
// @Description Retrieve comprehensive feedback statistics for the authenticated user's company including total likes, dislikes, and percentage calculations.
// @Tags Feedback
// @Accept json
// @Produce json
// @Success 200 {object} response.APIResponse{data=CompanyFeedbackStatsResponse} "Company feedback statistics retrieved successfully"
// @Failure 400 {object} response.APIResponse{error=string,message=string} "Bad request - Missing company 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) GetCompanyFeedbackStats(c echo.Context) error {
companyID, err := user.GetCompanyIDFromContext(c)
if err != nil {
return response.BadRequest(c, "Company ID required", "User must be associated with a company")
}
stats, err := h.service.CompanyFeedbackStats(c.Request().Context(), companyID)
if err != nil {
return response.InternalServerError(c, "Failed to retrieve company feedback statistics")
}
return response.Success(c, stats, "Company feedback statistics retrieved successfully")
}