package handlers import ( "Yimaru-Backend/internal/domain" "errors" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" ) type createFAQReq struct { Question string `json:"question" validate:"required"` Answer string `json:"answer" validate:"required"` Category *string `json:"category"` DisplayOrder *int32 `json:"display_order"` Status *string `json:"status"` } type updateFAQReq struct { Question *string `json:"question"` Answer *string `json:"answer"` Category *string `json:"category"` DisplayOrder *int32 `json:"display_order"` Status *string `json:"status"` } type faqRes struct { ID int64 `json:"id"` Question string `json:"question"` Answer string `json:"answer"` Category *string `json:"category,omitempty"` DisplayOrder int32 `json:"display_order"` Status string `json:"status"` CreatedAt string `json:"created_at"` UpdatedAt *string `json:"updated_at,omitempty"` } type listFAQsRes struct { FAQs []faqRes `json:"faqs"` TotalCount int64 `json:"total_count"` } func mapFAQToRes(f domain.FAQ) faqRes { var updatedAt *string if f.UpdatedAt != nil { value := f.UpdatedAt.String() updatedAt = &value } return faqRes{ ID: f.ID, Question: f.Question, Answer: f.Answer, Category: f.Category, DisplayOrder: f.DisplayOrder, Status: f.Status, CreatedAt: f.CreatedAt.String(), UpdatedAt: updatedAt, } } // ListPublicFAQs godoc // @Summary List published FAQs // @Description Returns active FAQs for public/help center usage // @Tags faqs // @Produce json // @Param category query string false "Filter by category" // @Param limit query int false "Limit (default 50)" // @Param offset query int false "Offset (default 0)" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/faqs [get] func (h *Handler) ListPublicFAQs(c *fiber.Ctx) error { category := strings.TrimSpace(c.Query("category")) var categoryPtr *string if category != "" { categoryPtr = &category } limit, err := strconv.Atoi(c.Query("limit", "50")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid limit", Error: err.Error(), }) } offset, err := strconv.Atoi(c.Query("offset", "0")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid offset", Error: err.Error(), }) } status := domain.FAQStatusActive faqs, total, err := h.faqSvc.ListFAQs(c.Context(), &status, categoryPtr, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to list FAQs", Error: err.Error(), }) } out := make([]faqRes, 0, len(faqs)) for _, f := range faqs { out = append(out, mapFAQToRes(f)) } return c.JSON(domain.Response{ Message: "FAQs retrieved successfully", Data: listFAQsRes{ FAQs: out, TotalCount: total, }, }) } // GetPublicFAQByID godoc // @Summary Get published FAQ by ID // @Description Returns one active FAQ item // @Tags faqs // @Produce json // @Param id path int true "FAQ ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Router /api/v1/faqs/{id} [get] func (h *Handler) GetPublicFAQByID(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil || id <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid FAQ ID", Error: "id must be a positive integer", }) } faq, err := h.faqSvc.GetFAQByID(c.Context(), id, false) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "FAQ not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get FAQ", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "FAQ retrieved successfully", Data: mapFAQToRes(faq), }) } // ListFAQsAdmin godoc // @Summary List FAQs (admin) // @Description Returns FAQs for admin management with status/category filtering // @Tags faqs // @Produce json // @Param status query string false "ACTIVE or INACTIVE" // @Param category query string false "Filter by category" // @Param limit query int false "Limit (default 20)" // @Param offset query int false "Offset (default 0)" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/faqs [get] func (h *Handler) ListFAQsAdmin(c *fiber.Ctx) error { status := strings.ToUpper(strings.TrimSpace(c.Query("status"))) var statusPtr *string if status != "" { statusPtr = &status } category := strings.TrimSpace(c.Query("category")) var categoryPtr *string if category != "" { categoryPtr = &category } limit, err := strconv.Atoi(c.Query("limit", "20")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid limit", Error: err.Error(), }) } offset, err := strconv.Atoi(c.Query("offset", "0")) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid offset", Error: err.Error(), }) } faqs, total, err := h.faqSvc.ListFAQs(c.Context(), statusPtr, categoryPtr, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to list FAQs", Error: err.Error(), }) } out := make([]faqRes, 0, len(faqs)) for _, f := range faqs { out = append(out, mapFAQToRes(f)) } return c.JSON(domain.Response{ Message: "FAQs retrieved successfully", Data: listFAQsRes{ FAQs: out, TotalCount: total, }, }) } // GetFAQByIDAdmin godoc // @Summary Get FAQ by ID (admin) // @Description Returns one FAQ regardless of status // @Tags faqs // @Produce json // @Param id path int true "FAQ ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/faqs/{id} [get] func (h *Handler) GetFAQByIDAdmin(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil || id <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid FAQ ID", Error: "id must be a positive integer", }) } faq, err := h.faqSvc.GetFAQByID(c.Context(), id, true) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "FAQ not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get FAQ", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "FAQ retrieved successfully", Data: mapFAQToRes(faq), }) } // CreateFAQ godoc // @Summary Create FAQ // @Description Creates a new FAQ item // @Tags faqs // @Accept json // @Produce json // @Param body body createFAQReq true "Create FAQ payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/faqs [post] func (h *Handler) CreateFAQ(c *fiber.Ctx) error { var req createFAQReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } if valErrs, ok := h.validator.Validate(c, req); !ok { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Validation failed", Error: firstValidationError(valErrs), }) } faq, err := h.faqSvc.CreateFAQ(c.Context(), domain.CreateFAQInput{ Question: req.Question, Answer: req.Answer, Category: req.Category, DisplayOrder: req.DisplayOrder, Status: req.Status, }) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to create FAQ", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "FAQ created successfully", Data: mapFAQToRes(faq), }) } // UpdateFAQ godoc // @Summary Update FAQ // @Description Updates an existing FAQ item // @Tags faqs // @Accept json // @Produce json // @Param id path int true "FAQ ID" // @Param body body updateFAQReq true "Update FAQ payload" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/faqs/{id} [put] func (h *Handler) UpdateFAQ(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil || id <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid FAQ ID", Error: "id must be a positive integer", }) } var req updateFAQReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } faq, err := h.faqSvc.UpdateFAQ(c.Context(), id, domain.UpdateFAQInput{ Question: req.Question, Answer: req.Answer, Category: req.Category, DisplayOrder: req.DisplayOrder, Status: req.Status, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "FAQ not found", }) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to update FAQ", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "FAQ updated successfully", Data: mapFAQToRes(faq), }) } // DeleteFAQ godoc // @Summary Delete FAQ // @Description Deletes an FAQ item // @Tags faqs // @Produce json // @Param id path int true "FAQ ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/faqs/{id} [delete] func (h *Handler) DeleteFAQ(c *fiber.Ctx) error { id, err := strconv.ParseInt(c.Params("id"), 10, 64) if err != nil || id <= 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid FAQ ID", Error: "id must be a positive integer", }) } if err := h.faqSvc.DeleteFAQ(c.Context(), id); err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "FAQ not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to delete FAQ", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "FAQ deleted successfully", Data: fiber.Map{"id": id}, }) }