package handlers import ( "Yimaru-Backend/internal/domain" "errors" "strconv" "strings" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" ) type createEmailTemplateReq struct { Slug string `json:"slug" validate:"required"` Name string `json:"name" validate:"required"` Subject string `json:"subject" validate:"required"` BodyText string `json:"body_text" validate:"required"` BodyHTML string `json:"body_html" validate:"required"` Variables []string `json:"variables"` Status *string `json:"status"` } type updateEmailTemplateReq struct { Name *string `json:"name"` Subject *string `json:"subject"` BodyText *string `json:"body_text"` BodyHTML *string `json:"body_html"` Variables []string `json:"variables"` Status *string `json:"status"` } type previewEmailTemplateReq struct { Variables map[string]any `json:"variables"` } type emailTemplateRes struct { ID int64 `json:"id"` Slug string `json:"slug"` Name string `json:"name"` Subject string `json:"subject"` BodyText string `json:"body_text"` BodyHTML string `json:"body_html"` Variables []string `json:"variables"` IsSystem bool `json:"is_system"` Status string `json:"status"` CreatedAt string `json:"created_at"` UpdatedAt *string `json:"updated_at,omitempty"` } type listEmailTemplatesRes struct { Templates []emailTemplateRes `json:"templates"` TotalCount int64 `json:"total_count"` } func mapEmailTemplateToRes(t domain.EmailTemplate) emailTemplateRes { var updatedAt *string if t.UpdatedAt != nil { value := t.UpdatedAt.String() updatedAt = &value } variables := t.Variables if variables == nil { variables = []string{} } return emailTemplateRes{ ID: t.ID, Slug: t.Slug, Name: t.Name, Subject: t.Subject, BodyText: t.BodyText, BodyHTML: t.BodyHTML, Variables: variables, IsSystem: t.IsSystem, Status: t.Status, CreatedAt: t.CreatedAt.String(), UpdatedAt: updatedAt, } } // ListEmailTemplatesAdmin godoc // @Summary List email templates (admin) // @Description Returns email templates for admin management // @Tags email-templates // @Produce json // @Param status query string false "ACTIVE or INACTIVE" // @Param query query string false "Search by slug, name, or subject" // @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/email-templates [get] func (h *Handler) ListEmailTemplatesAdmin(c *fiber.Ctx) error { status := strings.ToUpper(strings.TrimSpace(c.Query("status"))) var statusPtr *string if status != "" { statusPtr = &status } search := strings.TrimSpace(c.Query("query")) var searchPtr *string if search != "" { searchPtr = &search } 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(), }) } templates, total, err := h.emailTemplateSvc.ListEmailTemplates(c.Context(), statusPtr, searchPtr, int32(limit), int32(offset)) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to list email templates", Error: err.Error(), }) } out := make([]emailTemplateRes, 0, len(templates)) for _, tmpl := range templates { out = append(out, mapEmailTemplateToRes(tmpl)) } return c.JSON(domain.Response{ Message: "Email templates retrieved successfully", Data: listEmailTemplatesRes{ Templates: out, TotalCount: total, }, }) } // GetEmailTemplateByIDAdmin godoc // @Summary Get email template by ID (admin) // @Description Returns one email template regardless of status // @Tags email-templates // @Produce json // @Param id path int true "Email template 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/email-templates/{id} [get] func (h *Handler) GetEmailTemplateByIDAdmin(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 email template ID", Error: "id must be a positive integer", }) } tmpl, err := h.emailTemplateSvc.GetEmailTemplateByID(c.Context(), id, true) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Email template not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template retrieved successfully", Data: mapEmailTemplateToRes(tmpl), }) } // GetEmailTemplateBySlugAdmin godoc // @Summary Get email template by slug (admin) // @Description Returns one email template by slug regardless of status // @Tags email-templates // @Produce json // @Param slug path string true "Email template slug" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/email-templates/slug/{slug} [get] func (h *Handler) GetEmailTemplateBySlugAdmin(c *fiber.Ctx) error { slug := strings.TrimSpace(c.Params("slug")) if slug == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid email template slug", Error: "slug is required", }) } tmpl, err := h.emailTemplateSvc.GetEmailTemplateBySlug(c.Context(), slug, true) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Email template not found", }) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to get email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template retrieved successfully", Data: mapEmailTemplateToRes(tmpl), }) } // CreateEmailTemplate godoc // @Summary Create email template // @Description Creates a new custom email template // @Tags email-templates // @Accept json // @Produce json // @Param body body createEmailTemplateReq true "Create email template payload" // @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/email-templates [post] func (h *Handler) CreateEmailTemplate(c *fiber.Ctx) error { var req createEmailTemplateReq 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), }) } tmpl, err := h.emailTemplateSvc.CreateEmailTemplate(c.Context(), domain.CreateEmailTemplateInput{ Slug: req.Slug, Name: req.Name, Subject: req.Subject, BodyText: req.BodyText, BodyHTML: req.BodyHTML, Variables: req.Variables, Status: req.Status, }) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to create email template", Error: err.Error(), }) } return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Email template created successfully", Data: mapEmailTemplateToRes(tmpl), }) } // UpdateEmailTemplate godoc // @Summary Update email template // @Description Updates an existing email template // @Tags email-templates // @Accept json // @Produce json // @Param id path int true "Email template ID" // @Param body body updateEmailTemplateReq true "Update email template 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/email-templates/{id} [put] func (h *Handler) UpdateEmailTemplate(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 email template ID", Error: "id must be a positive integer", }) } var req updateEmailTemplateReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } tmpl, err := h.emailTemplateSvc.UpdateEmailTemplate(c.Context(), id, domain.UpdateEmailTemplateInput{ Name: req.Name, Subject: req.Subject, BodyText: req.BodyText, BodyHTML: req.BodyHTML, Variables: req.Variables, Status: req.Status, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Email template not found", }) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to update email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template updated successfully", Data: mapEmailTemplateToRes(tmpl), }) } // DeleteEmailTemplate godoc // @Summary Delete email template // @Description Deletes a custom email template // @Tags email-templates // @Produce json // @Param id path int true "Email template 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/email-templates/{id} [delete] func (h *Handler) DeleteEmailTemplate(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 email template ID", Error: "id must be a positive integer", }) } if err := h.emailTemplateSvc.DeleteEmailTemplate(c.Context(), id); err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Email template not found", }) } return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to delete email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template deleted successfully", Data: fiber.Map{"id": id}, }) } // PreviewEmailTemplateBySlug godoc // @Summary Preview email template by slug // @Description Renders an email template with sample variables without sending // @Tags email-templates // @Accept json // @Produce json // @Param slug path string true "Email template slug" // @Param body body previewEmailTemplateReq true "Preview variables" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/email-templates/slug/{slug}/preview [post] func (h *Handler) PreviewEmailTemplateBySlug(c *fiber.Ctx) error { slug := strings.TrimSpace(c.Params("slug")) if slug == "" { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid email template slug", Error: "slug is required", }) } var req previewEmailTemplateReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } rendered, err := h.emailTemplateSvc.PreviewEmailTemplate(c.Context(), domain.PreviewEmailTemplateInput{ Slug: slug, Variables: req.Variables, }) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to preview email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template preview generated successfully", Data: rendered, }) } // PreviewEmailTemplateByID godoc // @Summary Preview email template by ID // @Description Renders an email template with sample variables without sending // @Tags email-templates // @Accept json // @Produce json // @Param id path int true "Email template ID" // @Param body body previewEmailTemplateReq true "Preview variables" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/admin/email-templates/{id}/preview [post] func (h *Handler) PreviewEmailTemplateByID(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 email template ID", Error: "id must be a positive integer", }) } var req previewEmailTemplateReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", Error: err.Error(), }) } tmpl, err := h.emailTemplateSvc.GetEmailTemplateByID(c.Context(), id, true) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ Message: "Email template not found", }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to get email template", Error: err.Error(), }) } rendered, err := h.emailTemplateSvc.PreviewEmailTemplate(c.Context(), domain.PreviewEmailTemplateInput{ Slug: tmpl.Slug, Variables: req.Variables, }) if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Failed to preview email template", Error: err.Error(), }) } return c.JSON(domain.Response{ Message: "Email template preview generated successfully", Data: rendered, }) }