package handlers import ( "net/http" "github.com/flatrender/notification-svc/internal/db" "github.com/flatrender/notification-svc/internal/middleware" "github.com/flatrender/notification-svc/internal/models" "github.com/flatrender/notification-svc/internal/sender" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // Max recipients per campaign send (V1 sends synchronously). const campaignRecipientCap = 1000 type CampaignHandler struct{ store *db.Store } func NewCampaignHandler(s *db.Store) *CampaignHandler { return &CampaignHandler{store: s} } func (h *CampaignHandler) tenant(c *gin.Context) uuid.UUID { v, _ := c.Get(middleware.CtxTenantID) id, _ := v.(uuid.UUID) return id } func (h *CampaignHandler) uid(c *gin.Context) uuid.UUID { v, _ := c.Get(middleware.CtxUserID) id, _ := v.(uuid.UUID) return id } // GET /v1/campaigns func (h *CampaignHandler) List(c *gin.Context) { list, err := h.store.ListCampaigns(c.Request.Context(), h.tenant(c)) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": list}) } // POST /v1/campaigns func (h *CampaignHandler) Create(c *gin.Context) { var req models.CreateCampaignRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: err.Error()}) return } if req.Channel != "sms" && req.Channel != "email" { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "channel must be sms or email"}) return } camp, err := h.store.CreateCampaign(c.Request.Context(), h.tenant(c), req) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } c.JSON(http.StatusOK, camp) } // DELETE /v1/campaigns/:id func (h *CampaignHandler) Delete(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } if err := h.store.DeleteCampaign(c.Request.Context(), id, h.tenant(c)); err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } // POST /v1/campaigns/:id/send func (h *CampaignHandler) Send(c *gin.Context) { ctx := c.Request.Context() id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } tenant := h.tenant(c) camp, _ := h.store.GetCampaign(ctx, id, tenant) if camp == nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "campaign not found"}) return } cfg, _ := h.store.GetChannelConfig(ctx, tenant, camp.Channel) if cfg == nil || !cfg.Enabled { c.JSON(http.StatusBadRequest, models.APIError{Code: "not_configured", Message: camp.Channel + " channel is not configured/enabled"}) return } // Resolve subject/body (from template for email, else campaign fields). subject, body := "", "" if camp.Subject != nil { subject = *camp.Subject } if camp.BodyHTML != nil { body = *camp.BodyHTML } if camp.Channel == "email" && camp.TemplateCode != nil && *camp.TemplateCode != "" { if tpl, _ := h.store.GetEmailTemplate(ctx, *camp.TemplateCode, "fa"); tpl != nil { if tpl.Subject != nil { subject = *tpl.Subject } if tpl.BodyHTML != nil { body = *tpl.BodyHTML } } } recipients, err := h.store.ResolveAudience(ctx, tenant, camp.Channel, camp.Audience, campaignRecipientCap) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } total := len(recipients) _ = h.store.UpdateCampaignResult(ctx, id, "Sending", total, 0, 0) var scfg sender.SMTPConfig if camp.Channel == "email" { scfg = sender.SMTPConfig{ Host: str(cfg.Settings["host"]), Port: intv(cfg.Settings["port"]), Username: str(cfg.Settings["username"]), Password: str(cfg.Settings["password"]), FromEmail: str(cfg.Settings["from_email"]), FromName: str(cfg.Settings["from_name"]), UseTLS: boolv(cfg.Settings["use_tls"]), } } sent, failed := 0, 0 provider := map[string]string{"sms": "kavenegar", "email": "smtp"}[camp.Channel] chLabel := map[string]string{"sms": "SMS", "email": "Email"}[camp.Channel] for _, r := range recipients { var sendErr error if camp.Channel == "email" { sendErr = sender.SendEmail(scfg, r, subject, body) } else { _, sendErr = sender.SendSMS(str(cfg.Settings["api_key"]), str(cfg.Settings["line_number"]), r, body) } status := "Sent" var em *string if sendErr != nil { failed++ status = "Failed" e := sendErr.Error() em = &e } else { sent++ } subj := &subject _ = h.store.LogDelivery(ctx, tenant, h.uid(c), chLabel, r, subj, &provider, nil, &status, em) } final := "Sent" if total > 0 && sent == 0 { final = "Failed" } _ = h.store.UpdateCampaignResult(ctx, id, final, total, sent, failed) c.JSON(http.StatusOK, gin.H{"status": final, "total": total, "sent": sent, "failed": failed}) }