buhapp-backend/internal/handlers/reviews.go
ga 1eb7af6bac Sprint 5: reviews and ratings
- POST /api/v1/reviews (one per chat_id, UNIQUE)
- GET  /api/v1/users/:id/reviews (public list)
- GET  /api/v1/users/:id/stats (rating_avg, count)
- migration 0004: reviews (rating 1-5, anonymous flag), user_stats aggregate
- only chat participants can review; reviewed_id is other party
2026-08-20 16:20:15 +00:00

127 lines
3.7 KiB
Go

package handlers
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/buhapp/backend/internal/audit"
"github.com/buhapp/backend/internal/chat"
"github.com/buhapp/backend/internal/reviews"
)
type ReviewHandlers struct {
Reviews *reviews.Repo
Chat *chat.Repo
Audit *audit.Repo
}
func NewReviewHandlers(r *reviews.Repo, ch *chat.Repo, a *audit.Repo) *ReviewHandlers {
return &ReviewHandlers{Reviews: r, Chat: ch, Audit: a}
}
type createReviewRequest struct {
ChatID string `json:"chat_id"`
Rating int `json:"rating"`
Body string `json:"body"`
Anonymous bool `json:"anonymous"`
}
func (h *ReviewHandlers) Create(c *fiber.Ctx) error {
me, err := userID(c)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "no user"})
}
var req createReviewRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON"})
}
chatID, err := uuid.Parse(req.ChatID)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad chat_id"})
}
if req.Rating < 1 || req.Rating > 5 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "rating must be 1..5"})
}
body := strings.TrimSpace(req.Body)
if len(body) > 2000 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "body too long (max 2000)"})
}
// Получаем чат и определяем reviewed_id
ch, err := h.Chat.GetByID(c.UserContext(), chatID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
if ch == nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "chat not found"})
}
reviewed := ch.UserA
if reviewed == me {
reviewed = ch.UserB
}
if ch.UserA != me && ch.UserB != me {
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "not a chat member"})
}
// Проверим, не оставлял ли уже
dup, _ := h.Reviews.HasReviewed(c.UserContext(), chatID, me)
if dup {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"error": "review already exists for this chat"})
}
rev := &reviews.Review{
ReviewerID: me,
ReviewedID: reviewed,
ChatID: chatID,
Rating: req.Rating,
Body: body,
Anonymous: req.Anonymous,
}
if err := h.Reviews.Create(c.UserContext(), rev); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
}
_ = h.Audit.Log(c.UserContext(), &audit.Event{
UserID: &me, Action: "review.create",
TargetType: "user", TargetID: reviewed.String(),
IP: c.IP(), UserAgent: c.Get("User-Agent"),
})
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"id": rev.ID,
"rating": rev.Rating,
"anonymous": rev.Anonymous,
"created_at": rev.CreatedAt,
})
}
func (h *ReviewHandlers) ListForUser(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad id"})
}
limit := c.QueryInt("limit", 20)
offset := c.QueryInt("offset", 0)
list, err := h.Reviews.ListForUser(c.UserContext(), id, limit, offset)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
return c.JSON(fiber.Map{
"reviews": list,
"count": len(list),
})
}
func (h *ReviewHandlers) Stats(c *fiber.Ctx) error {
id, err := uuid.Parse(c.Params("id"))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bad id"})
}
st, err := h.Reviews.GetStats(c.UserContext(), id)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "db error"})
}
return c.JSON(st)
}