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>
130 lines
3.1 KiB
Go
130 lines
3.1 KiB
Go
package faqs
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
"Yimaru-Backend/internal/ports"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type Service struct {
|
|
faqStore ports.FAQStore
|
|
}
|
|
|
|
func NewService(faqStore ports.FAQStore) *Service {
|
|
return &Service{faqStore: faqStore}
|
|
}
|
|
|
|
func normalizeFAQStatus(status *string) (string, error) {
|
|
if status == nil || strings.TrimSpace(*status) == "" {
|
|
return domain.FAQStatusActive, nil
|
|
}
|
|
value := strings.ToUpper(strings.TrimSpace(*status))
|
|
switch value {
|
|
case domain.FAQStatusActive, domain.FAQStatusInactive:
|
|
return value, nil
|
|
default:
|
|
return "", fmt.Errorf("status must be one of %s, %s", domain.FAQStatusActive, domain.FAQStatusInactive)
|
|
}
|
|
}
|
|
|
|
func (s *Service) CreateFAQ(ctx context.Context, input domain.CreateFAQInput) (domain.FAQ, error) {
|
|
input.Question = strings.TrimSpace(input.Question)
|
|
input.Answer = strings.TrimSpace(input.Answer)
|
|
if input.Question == "" {
|
|
return domain.FAQ{}, fmt.Errorf("question is required")
|
|
}
|
|
if input.Answer == "" {
|
|
return domain.FAQ{}, fmt.Errorf("answer is required")
|
|
}
|
|
if input.Category != nil {
|
|
c := strings.TrimSpace(*input.Category)
|
|
if c == "" {
|
|
input.Category = nil
|
|
} else {
|
|
input.Category = &c
|
|
}
|
|
}
|
|
status, err := normalizeFAQStatus(input.Status)
|
|
if err != nil {
|
|
return domain.FAQ{}, err
|
|
}
|
|
input.Status = &status
|
|
return s.faqStore.CreateFAQ(ctx, input)
|
|
}
|
|
|
|
func (s *Service) UpdateFAQ(ctx context.Context, id int64, input domain.UpdateFAQInput) (domain.FAQ, error) {
|
|
if id <= 0 {
|
|
return domain.FAQ{}, fmt.Errorf("invalid faq id")
|
|
}
|
|
if input.Question != nil {
|
|
trimmed := strings.TrimSpace(*input.Question)
|
|
if trimmed == "" {
|
|
return domain.FAQ{}, fmt.Errorf("question cannot be empty")
|
|
}
|
|
input.Question = &trimmed
|
|
}
|
|
if input.Answer != nil {
|
|
trimmed := strings.TrimSpace(*input.Answer)
|
|
if trimmed == "" {
|
|
return domain.FAQ{}, fmt.Errorf("answer cannot be empty")
|
|
}
|
|
input.Answer = &trimmed
|
|
}
|
|
if input.Category != nil {
|
|
c := strings.TrimSpace(*input.Category)
|
|
input.Category = &c
|
|
}
|
|
if input.Status != nil {
|
|
status, err := normalizeFAQStatus(input.Status)
|
|
if err != nil {
|
|
return domain.FAQ{}, err
|
|
}
|
|
input.Status = &status
|
|
}
|
|
return s.faqStore.UpdateFAQ(ctx, id, input)
|
|
}
|
|
|
|
func (s *Service) GetFAQByID(ctx context.Context, id int64, includeInactive bool) (domain.FAQ, error) {
|
|
if id <= 0 {
|
|
return domain.FAQ{}, fmt.Errorf("invalid faq id")
|
|
}
|
|
return s.faqStore.GetFAQByID(ctx, id, includeInactive)
|
|
}
|
|
|
|
func (s *Service) ListFAQs(ctx context.Context, status *string, category *string, limit int32, offset int32) ([]domain.FAQ, int64, error) {
|
|
if status != nil {
|
|
normalized, err := normalizeFAQStatus(status)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
status = &normalized
|
|
}
|
|
if category != nil {
|
|
trimmed := strings.TrimSpace(*category)
|
|
if trimmed == "" {
|
|
category = nil
|
|
} else {
|
|
category = &trimmed
|
|
}
|
|
}
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
return s.faqStore.ListFAQs(ctx, status, category, limit, offset)
|
|
}
|
|
|
|
func (s *Service) DeleteFAQ(ctx context.Context, id int64) error {
|
|
if id <= 0 {
|
|
return fmt.Errorf("invalid faq id")
|
|
}
|
|
return s.faqStore.DeleteFAQ(ctx, id)
|
|
}
|