100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
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()
|
|
}
|