Yimaru-BackEnd/internal/repository/faqs.go
Yared Yemane 6a4fe68628 Add full FAQ management APIs and integration assets.
Implement public FAQ read endpoints and admin CRUD with RBAC, persistence, and migrations, then regenerate Swagger and add a complete Postman collection so frontend/admin teams can integrate and validate the feature end-to-end.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-11 07:58:17 -07:00

200 lines
5.4 KiB
Go

package repository
import (
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/ports"
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
func NewFAQStore(s *Store) ports.FAQStore { return s }
func faqToDomain(
id int64,
question string,
answer string,
category pgtype.Text,
displayOrder int32,
status string,
createdAt pgtype.Timestamptz,
updatedAt pgtype.Timestamptz,
) domain.FAQ {
return domain.FAQ{
ID: id,
Question: question,
Answer: answer,
Category: fromPgText(category),
DisplayOrder: displayOrder,
Status: status,
CreatedAt: createdAt.Time,
UpdatedAt: timePtr(updatedAt),
}
}
func (s *Store) CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error) {
displayOrder := int32(0)
if input.DisplayOrder != nil {
displayOrder = *input.DisplayOrder
}
status := domain.FAQStatusActive
if input.Status != nil {
status = *input.Status
}
row := s.conn.QueryRow(ctx, `
INSERT INTO faqs (question, answer, category, display_order, status)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
`, input.Question, input.Answer, toPgText(input.Category), displayOrder, status)
var (
id int64
question string
answer string
category pgtype.Text
orderVal int32
faqStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&id, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
return domain.FAQ{}, err
}
return faqToDomain(id, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
}
func (s *Store) UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error) {
categorySet := input.Category != nil
var categoryValue pgtype.Text
if categorySet {
if *input.Category == "" {
categoryValue = pgtype.Text{Valid: false}
} else {
categoryValue = pgtype.Text{String: *input.Category, Valid: true}
}
}
row := s.conn.QueryRow(ctx, `
UPDATE faqs
SET question = COALESCE($2, question),
answer = COALESCE($3, answer),
category = CASE WHEN $4::boolean THEN $5::text ELSE category END,
display_order = COALESCE($6, display_order),
status = COALESCE($7, status),
updated_at = NOW()
WHERE id = $1
RETURNING id, question, answer, category, display_order, status, created_at, updated_at
`,
id,
input.Question,
input.Answer,
categorySet,
categoryValue,
input.DisplayOrder,
input.Status,
)
var (
faqID int64
question string
answer string
rowCategory pgtype.Text
orderVal int32
faqStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
return domain.FAQ{}, err
}
return faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt), nil
}
func (s *Store) GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error) {
row := s.conn.QueryRow(ctx, `
SELECT id, question, answer, category, display_order, status, created_at, updated_at
FROM faqs
WHERE id = $1
AND ($2::boolean = TRUE OR status = 'ACTIVE')
`, id, includeInactive)
var (
faqID int64
question string
answer string
category pgtype.Text
orderVal int32
faqStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := row.Scan(&faqID, &question, &answer, &category, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
return domain.FAQ{}, err
}
return faqToDomain(faqID, question, answer, category, orderVal, faqStatus, createdAt, updatedAt), nil
}
func (s *Store) ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error) {
rows, err := s.conn.Query(ctx, `
SELECT id, question, answer, category, display_order, status, created_at, updated_at
FROM faqs
WHERE ($1::text IS NULL OR status = $1)
AND ($2::text IS NULL OR category = $2)
ORDER BY display_order ASC, id ASC
LIMIT $3 OFFSET $4
`, toPgText(status), toPgText(category), limit, offset)
if err != nil {
return nil, 0, err
}
defer rows.Close()
faqs := make([]domain.FAQ, 0)
for rows.Next() {
var (
faqID int64
question string
answer string
rowCategory pgtype.Text
orderVal int32
faqStatus string
createdAt pgtype.Timestamptz
updatedAt pgtype.Timestamptz
)
if err := rows.Scan(&faqID, &question, &answer, &rowCategory, &orderVal, &faqStatus, &createdAt, &updatedAt); err != nil {
return nil, 0, err
}
faqs = append(faqs, faqToDomain(faqID, question, answer, rowCategory, orderVal, faqStatus, createdAt, updatedAt))
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
var totalCount int64
if err := s.conn.QueryRow(ctx, `
SELECT COUNT(*)
FROM faqs
WHERE ($1::text IS NULL OR status = $1)
AND ($2::text IS NULL OR category = $2)
`, toPgText(status), toPgText(category)).Scan(&totalCount); err != nil {
return nil, 0, err
}
return faqs, totalCount, nil
}
func (s *Store) DeleteFAQ(ctx context.Context, id int64) error {
cmd, err := s.conn.Exec(ctx, `DELETE FROM faqs WHERE id = $1`, id)
if err != nil {
return err
}
if cmd.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}