package repository import ( "context" "errors" "fmt" "github.com/prabhupadasays/spsays-bff/internal/model" ) var ( ErrFeedbackCategoryNotFound = errors.New("feedback category not found") ErrFeedbackCategoryInUse = errors.New("feedback is filed under this category; archive it instead") ErrFeedbackCategoryExists = errors.New("a category with that name already exists") ) // FeedbackCategoryRepo stores the /feedback form's categories. type FeedbackCategoryRepo struct{ pg *Postgres } func NewFeedbackCategoryRepo(pg *Postgres) *FeedbackCategoryRepo { return &FeedbackCategoryRepo{pg: pg} } // List returns categories in form order. includeArchived is true for the // admin (which also gets usage counts), false for the public form. func (r *FeedbackCategoryRepo) List(ctx context.Context, includeArchived bool) ([]model.FeedbackCategory, error) { rows, err := r.pg.Pool.Query(ctx, ` SELECT c.key, c.label, c.description, c.sort_order, c.archived_at, CASE WHEN $1 THEN (SELECT count(*) FROM feedback f WHERE f.category = c.key) ELSE 0 END FROM feedback_categories c WHERE $1 OR c.archived_at IS NULL ORDER BY (c.archived_at IS NOT NULL), c.sort_order, c.label`, includeArchived) if err != nil { return nil, fmt.Errorf("list feedback categories: %w", err) } defer rows.Close() out := []model.FeedbackCategory{} for rows.Next() { var c model.FeedbackCategory if err := rows.Scan(&c.Key, &c.Label, &c.Description, &c.SortOrder, &c.ArchivedAt, &c.UsageCount); err != nil { return nil, fmt.Errorf("scan feedback category: %w", err) } out = append(out, c) } return out, rows.Err() } // IsActive reports whether new feedback may be filed under key. func (r *FeedbackCategoryRepo) IsActive(ctx context.Context, key string) (bool, error) { var ok bool err := r.pg.Pool.QueryRow(ctx, ` SELECT EXISTS (SELECT 1 FROM feedback_categories WHERE key = $1 AND archived_at IS NULL)`, key).Scan(&ok) if err != nil { return false, fmt.Errorf("check feedback category: %w", err) } return ok, nil } // KeyTaken reports whether a key is already used, archived or not. func (r *FeedbackCategoryRepo) KeyTaken(ctx context.Context, key string) (bool, error) { var ok bool err := r.pg.Pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM feedback_categories WHERE key = $1)`, key).Scan(&ok) return ok, err } // Create appends a category at the end of the form. func (r *FeedbackCategoryRepo) Create(ctx context.Context, key, label, description string) (*model.FeedbackCategory, error) { c := model.FeedbackCategory{Key: key, Label: label, Description: description} err := r.pg.Pool.QueryRow(ctx, ` INSERT INTO feedback_categories (key, label, description, sort_order) VALUES ($1, $2, $3, COALESCE((SELECT max(sort_order) + 1 FROM feedback_categories WHERE archived_at IS NULL), 0)) RETURNING sort_order`, key, label, description).Scan(&c.SortOrder) if isUniqueViolation(err, "feedback_categories_pkey") { return nil, ErrFeedbackCategoryExists } if err != nil { return nil, fmt.Errorf("create feedback category: %w", err) } return &c, nil } // Update changes a category's wording and archived state. Its key — what // past feedback points at — is never changed. func (r *FeedbackCategoryRepo) Update(ctx context.Context, key, label, description string, archived bool) error { tag, err := r.pg.Pool.Exec(ctx, ` UPDATE feedback_categories SET label = $2, description = $3, archived_at = CASE WHEN $4 THEN COALESCE(archived_at, now()) ELSE NULL END WHERE key = $1`, key, label, description, archived) if err != nil { return fmt.Errorf("update feedback category: %w", err) } if tag.RowsAffected() == 0 { return ErrFeedbackCategoryNotFound } return nil } // Reorder sets the form order to the given keys, first to last. func (r *FeedbackCategoryRepo) Reorder(ctx context.Context, keys []string) error { tx, err := r.pg.Pool.Begin(ctx) if err != nil { return fmt.Errorf("begin reorder: %w", err) } defer tx.Rollback(ctx) //nolint:errcheck for i, k := range keys { if _, err := tx.Exec(ctx, `UPDATE feedback_categories SET sort_order = $2 WHERE key = $1`, k, i); err != nil { return fmt.Errorf("reorder feedback category: %w", err) } } return tx.Commit(ctx) } // Delete removes a category nothing has been filed under. One that's in use // can only be archived, so no submission is ever left without a label. func (r *FeedbackCategoryRepo) Delete(ctx context.Context, key string) error { var used bool err := r.pg.Pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM feedback WHERE category = $1)`, key).Scan(&used) if err != nil { return fmt.Errorf("check category usage: %w", err) } if used { return ErrFeedbackCategoryInUse } tag, err := r.pg.Pool.Exec(ctx, ` DELETE FROM feedback_categories c WHERE c.key = $1 AND NOT EXISTS (SELECT 1 FROM feedback f WHERE f.category = c.key)`, key) if err != nil { return fmt.Errorf("delete feedback category: %w", err) } if tag.RowsAffected() == 0 { // Gone already, or feedback arrived between the check and the delete. if ok, _ := r.KeyTaken(ctx, key); ok { return ErrFeedbackCategoryInUse } return ErrFeedbackCategoryNotFound } return nil }