buhapp-backend/internal/reviews/repo.go
ga 778a2da4ef Sprint 5.1: JSON snake_case tags + lowercased fields
- All response types now use lowercase json tags (id, name, photo_url, etc.)
- email/phone/birthdate/last_seen_at/photo_url use omitempty
- audit.Metadata type []byte (was map[string]any)
- fixes register/login inconsistency where login returned ID/Name (CamelCase)
  while register returned id/name (lowercase)
2026-08-20 16:29:02 +00:00

152 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package reviews
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Review struct {
ID uuid.UUID `json:"id"`
ReviewerID uuid.UUID `json:"reviewer_id"`
ReviewedID uuid.UUID `json:"reviewed_id"`
ChatID uuid.UUID `json:"chat_id"`
Rating int `json:"rating"`
Body string `json:"body"`
Anonymous bool `json:"anonymous"`
CreatedAt time.Time `json:"created_at"`
}
type Stats struct {
UserID uuid.UUID `json:"user_id"`
RatingAvg float64 `json:"rating_avg"`
RatingCount int `json:"rating_count"`
ReviewsLeft int `json:"reviews_left"`
LastActiveAt *time.Time `json:"last_active_at,omitempty"`
}
type Repo struct {
pool *pgxpool.Pool
}
func NewRepo(pool *pgxpool.Pool) *Repo {
return &Repo{pool: pool}
}
// Create — создать отзыв. Один отзыв на chat_id (UNIQUE).
// Также обновит агрегаты в user_stats.
func (r *Repo) Create(ctx context.Context, rev *Review) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var exists bool
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM chats WHERE id=$1 AND (user_a=$2 OR user_b=$2))`,
rev.ChatID, rev.ReviewerID,
).Scan(&exists)
if err != nil {
return err
}
if !exists {
return errors.New("chat not found or not a participant")
}
var reviewer, reviewed *string
err = tx.QueryRow(ctx, `
INSERT INTO reviews (reviewer_id, reviewed_id, chat_id, rating, body, anonymous)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6)
RETURNING id, created_at, reviewer_id::text, reviewed_id::text`,
rev.ReviewerID, rev.ReviewedID, rev.ChatID, rev.Rating, rev.Body, rev.Anonymous,
).Scan(&rev.ID, &rev.CreatedAt, &reviewer, &reviewed)
if err != nil {
return err
}
// upsert stats для reviewed
_, err = tx.Exec(ctx, `
INSERT INTO user_stats (user_id, rating_avg, rating_count, last_active_at)
VALUES ($1, $2, 1, NOW())
ON CONFLICT (user_id) DO UPDATE
SET rating_avg = ((user_stats.rating_avg * user_stats.rating_count) + EXCLUDED.rating_avg) / (user_stats.rating_count + 1),
rating_count = user_stats.rating_count + 1,
last_active_at = NOW()`,
rev.ReviewedID, float64(rev.Rating),
)
if err != nil {
return err
}
// bump reviews_left для reviewer
_, err = tx.Exec(ctx, `
INSERT INTO user_stats (user_id, reviews_left, last_active_at)
VALUES ($1, 1, NOW())
ON CONFLICT (user_id) DO UPDATE
SET reviews_left = user_stats.reviews_left + 1,
last_active_at = NOW()`,
rev.ReviewerID,
)
if err != nil {
return err
}
return tx.Commit(ctx)
}
// ListForUser — публичные отзывы о пользователе
func (r *Repo) ListForUser(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Review, error) {
if limit <= 0 || limit > 100 {
limit = 20
}
rows, err := r.pool.Query(ctx, `
SELECT id, reviewer_id, reviewed_id, chat_id, rating,
COALESCE(body, ''), anonymous, created_at
FROM reviews
WHERE reviewed_id=$1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`, userID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Review
for rows.Next() {
var x Review
if err := rows.Scan(&x.ID, &x.ReviewerID, &x.ReviewedID, &x.ChatID, &x.Rating,
&x.Body, &x.Anonymous, &x.CreatedAt); err != nil {
return nil, err
}
out = append(out, x)
}
return out, rows.Err()
}
func (r *Repo) GetStats(ctx context.Context, userID uuid.UUID) (*Stats, error) {
s := &Stats{UserID: userID}
err := r.pool.QueryRow(ctx, `
SELECT rating_avg, rating_count, reviews_left, last_active_at
FROM user_stats WHERE user_id=$1`, userID,
).Scan(&s.RatingAvg, &s.RatingCount, &s.ReviewsLeft, &s.LastActiveAt)
if errors.Is(err, pgx.ErrNoRows) {
return s, nil
}
if err != nil {
return nil, err
}
return s, nil
}
// HasReviewed — оставлял ли reviewer уже отзыв на этот чат
func (r *Repo) HasReviewed(ctx context.Context, chatID, reviewerID uuid.UUID) (bool, error) {
var ok bool
err := r.pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM reviews WHERE chat_id=$1 AND reviewer_id=$2)`,
chatID, reviewerID,
).Scan(&ok)
return ok, err
}