package handlers import ( "Yimaru-Backend/internal/domain" "strconv" "github.com/gofiber/fiber/v2" ) type submitRatingReq struct { TargetType string `json:"target_type" validate:"required"` TargetID int64 `json:"target_id"` Stars int16 `json:"stars" validate:"required,min=1,max=5"` Review *string `json:"review"` } type ratingRes struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` TargetType string `json:"target_type"` TargetID int64 `json:"target_id"` Stars int16 `json:"stars"` Review *string `json:"review"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } type ratingSummaryRes struct { TotalCount int64 `json:"total_count"` AverageStars float64 `json:"average_stars"` } func mapRatingToRes(r domain.Rating) ratingRes { return ratingRes{ ID: r.ID, UserID: r.UserID, TargetType: string(r.TargetType), TargetID: r.TargetID, Stars: r.Stars, Review: r.Review, CreatedAt: r.CreatedAt.String(), UpdatedAt: r.UpdatedAt.String(), } } func isValidTargetType(t string) bool { switch domain.RatingTargetType(t) { case domain.RatingTargetApp, domain.RatingTargetCourse, domain.RatingTargetSubCourse: return true } return false } // SubmitRating godoc // @Summary Submit a rating // @Description Submit a rating for an app, course, or sub-course // @Tags ratings // @Accept json // @Produce json // @Param body body submitRatingReq true "Submit rating payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings [post] func (h *Handler) SubmitRating(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) var req submitRatingReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if !isValidTargetType(req.TargetType) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_type, must be one of: app, course, sub_course", }) } targetType := domain.RatingTargetType(req.TargetType) targetID := req.TargetID if targetType == domain.RatingTargetApp { targetID = 0 } rating, err := h.ratingSvc.SubmitRating(c.Context(), userID, targetType, targetID, req.Stars, req.Review) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to submit rating", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Rating submitted successfully", Data: mapRatingToRes(rating), }) } // GetMyRating godoc // @Summary Get my rating for a target // @Description Returns the current user's rating for a specific target // @Tags ratings // @Produce json // @Param target_type query string true "Target type (app, course, sub_course)" // @Param target_id query int true "Target ID (0 for app)" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings/me [get] func (h *Handler) GetMyRating(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) targetType := c.Query("target_type") if !isValidTargetType(targetType) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_type, must be one of: app, course, sub_course", }) } targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_id", Error: err.Error(), }) } if domain.RatingTargetType(targetType) == domain.RatingTargetApp { targetID = 0 } rating, err := h.ratingSvc.GetMyRating(c.Context(), userID, domain.RatingTargetType(targetType), targetID) if err != nil { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Rating not found", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Rating retrieved successfully", Data: mapRatingToRes(rating), }) } // GetRatingsByTarget godoc // @Summary Get ratings for a target // @Description Returns paginated ratings for a specific target // @Tags ratings // @Produce json // @Param target_type query string true "Target type (app, course, sub_course)" // @Param target_id query int true "Target ID (0 for app)" // @Param limit query int false "Limit (default 20)" // @Param offset query int false "Offset (default 0)" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings [get] func (h *Handler) GetRatingsByTarget(c *fiber.Ctx) error { targetType := c.Query("target_type") if !isValidTargetType(targetType) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_type, must be one of: app, course, sub_course", }) } targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_id", Error: err.Error(), }) } if domain.RatingTargetType(targetType) == domain.RatingTargetApp { targetID = 0 } limit, err := strconv.ParseInt(c.Query("limit", "20"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid limit", Error: err.Error(), }) } offset, err := strconv.ParseInt(c.Query("offset", "0"), 10, 32) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid offset", Error: err.Error(), }) } ratings, err := h.ratingSvc.GetRatingsByTarget(c.Context(), domain.RatingTargetType(targetType), targetID, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve ratings", Error: err.Error(), }) } res := make([]ratingRes, len(ratings)) for i, r := range ratings { res[i] = mapRatingToRes(r) } return c.JSON(domain.Response{ Message: "Ratings retrieved successfully", Data: res, }) } // GetRatingSummary godoc // @Summary Get rating summary for a target // @Description Returns the total count and average stars for a specific target // @Tags ratings // @Produce json // @Param target_type query string true "Target type (app, course, sub_course)" // @Param target_id query int true "Target ID (0 for app)" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings/summary [get] func (h *Handler) GetRatingSummary(c *fiber.Ctx) error { targetType := c.Query("target_type") if !isValidTargetType(targetType) { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_type, must be one of: app, course, sub_course", }) } targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid target_id", Error: err.Error(), }) } if domain.RatingTargetType(targetType) == domain.RatingTargetApp { targetID = 0 } summary, err := h.ratingSvc.GetRatingSummary(c.Context(), domain.RatingTargetType(targetType), targetID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve rating summary", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Rating summary retrieved successfully", Data: ratingSummaryRes{ TotalCount: summary.TotalCount, AverageStars: summary.AverageStars, }, }) } // GetMyRatings godoc // @Summary Get all my ratings // @Description Returns all ratings submitted by the current user // @Tags ratings // @Produce json // @Success 200 {object} domain.Response // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings/me/all [get] func (h *Handler) GetMyRatings(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) ratings, err := h.ratingSvc.GetUserRatings(c.Context(), userID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to retrieve ratings", Error: err.Error(), }) } res := make([]ratingRes, len(ratings)) for i, r := range ratings { res[i] = mapRatingToRes(r) } return c.JSON(domain.Response{ Message: "Ratings retrieved successfully", Data: res, }) } // DeleteRating godoc // @Summary Delete a rating // @Description Deletes a rating by ID for the current user // @Tags ratings // @Produce json // @Param id path int true "Rating ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/ratings/{id} [delete] func (h *Handler) DeleteRating(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) ratingID, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid rating ID", Error: err.Error(), }) } if err := h.ratingSvc.DeleteRating(c.Context(), ratingID, userID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete rating", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Rating deleted successfully", }) }