package repository import ( "context" "fmt" "github.com/prabhupadasays/spsays-bff/internal/model" ) // FeedbackRepo wraps Postgres access for the feedback table. type FeedbackRepo struct{ pg *Postgres } func NewFeedbackRepo(pg *Postgres) *FeedbackRepo { return &FeedbackRepo{pg: pg} } // Create inserts a single feedback row. userID is a pointer so the // authed and anonymous paths share one method; contactEmail is also a // pointer because authed submissions don't carry one (we read the // submitter's email from the joined user row at notify time). func (r *FeedbackRepo) Create(ctx context.Context, userID *string, category, message string, contactEmail *string) (*model.Feedback, error) { f := &model.Feedback{} err := r.pg.Pool.QueryRow(ctx, ` INSERT INTO feedback (user_id, category, message, contact_email) VALUES ($1, $2, $3, $4) RETURNING id, user_id, category, message, contact_email, created_at`, userID, category, message, contactEmail, ).Scan(&f.ID, &f.UserID, &f.Category, &f.Message, &f.ContactEmail, &f.CreatedAt) if err != nil { return nil, fmt.Errorf("insert feedback: %w", err) } return f, nil } // FindByID returns one feedback row joined with the submitter's // handle + name + email if a user_id is present. Used after Create to // assemble the admin notification email with the proper author block, // and after MarkResolved to assemble the author thank-you email. func (r *FeedbackRepo) FindByID(ctx context.Context, id string) (*model.Feedback, error) { f := &model.Feedback{} err := r.pg.Pool.QueryRow(ctx, ` SELECT f.id, f.user_id, f.category, f.message, f.contact_email, f.created_at, f.resolved_at, u.handle, u.name, u.email FROM feedback f LEFT JOIN users u ON u.id = f.user_id WHERE f.id = $1`, id, ).Scan(&f.ID, &f.UserID, &f.Category, &f.Message, &f.ContactEmail, &f.CreatedAt, &f.ResolvedAt, &f.AuthorHandle, &f.AuthorName, &f.AuthorEmail) if err != nil { return nil, fmt.Errorf("find feedback: %w", err) } return f, nil } // MarkResolved stamps resolved_at = now() on a still-open row. The // `resolved_at IS NULL` guard makes this idempotent and one-way: the // first call returns true (newly resolved), any subsequent call — or a // call on an unknown id — returns false so the caller knows NOT to fire // the thank-you email a second time. func (r *FeedbackRepo) MarkResolved(ctx context.Context, id string) (resolved bool, err error) { tag, err := r.pg.Pool.Exec(ctx, ` UPDATE feedback SET resolved_at = now() WHERE id = $1 AND resolved_at IS NULL`, id, ) if err != nil { return false, fmt.Errorf("mark feedback resolved: %w", err) } return tag.RowsAffected() == 1, nil } // Recent returns the most-recent N feedback rows joined with users // for display on /stats. NULL author columns mean anonymous (or the // original submitter's account was since deleted). func (r *FeedbackRepo) Recent(ctx context.Context, limit int) ([]model.Feedback, error) { if limit <= 0 || limit > 200 { limit = 25 } rows, err := r.pg.Pool.Query(ctx, ` SELECT f.id, f.user_id, f.category, f.message, f.contact_email, f.created_at, f.resolved_at, u.handle, u.name FROM feedback f LEFT JOIN users u ON u.id = f.user_id ORDER BY f.created_at DESC LIMIT $1`, limit, ) if err != nil { return nil, fmt.Errorf("query recent feedback: %w", err) } defer rows.Close() out := []model.Feedback{} for rows.Next() { var f model.Feedback if err := rows.Scan(&f.ID, &f.UserID, &f.Category, &f.Message, &f.ContactEmail, &f.CreatedAt, &f.ResolvedAt, &f.AuthorHandle, &f.AuthorName); err != nil { return nil, fmt.Errorf("scan feedback: %w", err) } out = append(out, f) } return out, rows.Err() }