package handler import ( "context" "encoding/json" "net/http" "strconv" "aiaa-notification-service/internal/cache" "aiaa-notification-service/internal/model" "aiaa-notification-service/internal/store" "github.com/gin-gonic/gin" ) type RuleHandler struct { store *store.Store cache *cache.Cache } func NewRuleHandler(s *store.Store, c *cache.Cache) *RuleHandler { return &RuleHandler{store: s, cache: c} } type createRuleReq struct { Name string `json:"name" binding:"required"` SourceName string `json:"source_name" binding:"required"` Event string `json:"event" binding:"required"` TemplateName string `json:"template_name" binding:"required"` Channels []string `json:"channels"` Conditions []model.Condition `json:"conditions,omitempty"` Enabled int `json:"enabled"` } func (h *RuleHandler) Create(c *gin.Context) { var req createRuleReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Enabled == 0 { req.Enabled = 1 } // Resolve names -> IDs src, err := h.store.GetSourceByName(c.Request.Context(), req.SourceName) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "source not found: " + req.SourceName}) return } tmpl, err := h.store.GetTemplateByName(c.Request.Context(), req.TemplateName) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "template not found: " + req.TemplateName}) return } channelIDs, err := resolveChannelNames(h.store, c, req.Channels) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var condsJSON *json.RawMessage if len(req.Conditions) > 0 { data, _ := json.Marshal(req.Conditions) raw := json.RawMessage(data) condsJSON = &raw } rule := &model.Rule{ Name: req.Name, SourceID: src.ID, Event: req.Event, TemplateID: tmpl.ID, Conditions: condsJSON, Enabled: req.Enabled, } if err := h.store.CreateRule(c.Request.Context(), rule, channelIDs); err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } rule.Channels = h.loadRuleChannels(c.Request.Context(), rule.ID) c.JSON(http.StatusCreated, rule) } func (h *RuleHandler) List(c *gin.Context) { var page store.PageFilter if err := c.ShouldBindQuery(&page); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } page.Normalize() rules, total, err := h.store.ListRules(c.Request.Context(), page) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := h.fillRuleChannels(c, rules); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": rules, "total": total, "page": page.Page}) } func (h *RuleHandler) Get(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) rule, err := h.store.GetRule(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) return } rule.Channels = h.loadRuleChannels(c.Request.Context(), id) c.JSON(http.StatusOK, rule) } // loadRuleChannels returns all channels bound to a rule, each with the per-rule // enabled switch, ordered by rule_channel id. func (h *RuleHandler) loadRuleChannels(ctx context.Context, ruleID int) []model.RuleChannelItem { byRule, err := h.store.ListRuleChannels(ctx, []int{ruleID}) if err != nil || len(byRule) == 0 { return []model.RuleChannelItem{} } rcs := byRule[ruleID] if len(rcs) == 0 { return []model.RuleChannelItem{} } items := make([]model.RuleChannelItem, 0, len(rcs)) for _, rc := range rcs { ch, err := h.store.GetChannel(ctx, rc.ChannelID) if err != nil { continue } items = append(items, model.RuleChannelItem{ ID: ch.ID, Name: ch.Name, Type: ch.Type, Enabled: rc.Enabled, }) } return items } // fillRuleChannels bulk-loads bound channels for rules and attaches them. func (h *RuleHandler) fillRuleChannels(ctx context.Context, rules []model.Rule) error { if len(rules) == 0 { return nil } ids := make([]int, 0, len(rules)) for i := range rules { ids = append(ids, rules[i].ID) } byRule, err := h.store.ListRuleChannels(ctx, ids) if err != nil { return err } if len(byRule) == 0 { return nil } for i := range rules { r := &rules[i] rcs := byRule[r.ID] if len(rcs) == 0 { r.Channels = []model.RuleChannelItem{} continue } items := make([]model.RuleChannelItem, 0, len(rcs)) for _, rc := range rcs { ch, err := h.store.GetChannel(ctx, rc.ChannelID) if err != nil { continue } items = append(items, model.RuleChannelItem{ ID: ch.ID, Name: ch.Name, Type: ch.Type, Enabled: rc.Enabled, }) } r.Channels = items } return nil } func (h *RuleHandler) Update(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var req createRuleReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } src, err := h.store.GetSourceByName(c.Request.Context(), req.SourceName) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "source not found"}) return } tmpl, err := h.store.GetTemplateByName(c.Request.Context(), req.TemplateName) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "template not found"}) return } channelIDs, err := resolveChannelNames(h.store, c, req.Channels) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var condsJSON *json.RawMessage if len(req.Conditions) > 0 { data, _ := json.Marshal(req.Conditions) raw := json.RawMessage(data) condsJSON = &raw } rule := &model.Rule{ Name: req.Name, SourceID: src.ID, Event: req.Event, TemplateID: tmpl.ID, Conditions: condsJSON, Enabled: req.Enabled, } if err := h.store.UpdateRule(c.Request.Context(), id, rule, channelIDs); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Invalidate cache if h.cache != nil { _ = h.cache.InvalidateRule(c.Request.Context(), src.ID, req.Event) _ = h.cache.InvalidateChannels(c.Request.Context(), id) } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *RuleHandler) Delete(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if err := h.store.DeleteRule(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *RuleHandler) Enable(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) rule, err := h.store.GetRule(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) return } if err := h.store.SetRuleEnabled(c.Request.Context(), id, true); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if h.cache != nil { _ = h.cache.InvalidateRule(c.Request.Context(), rule.SourceID, rule.Event) } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *RuleHandler) Disable(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) rule, err := h.store.GetRule(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "rule not found"}) return } if err := h.store.SetRuleEnabled(c.Request.Context(), id, false); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if h.cache != nil { _ = h.cache.InvalidateRule(c.Request.Context(), rule.SourceID, rule.Event) } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *RuleHandler) EnableChannel(c *gin.Context) { ruleID, _ := strconv.Atoi(c.Param("id")) channelID, _ := strconv.Atoi(c.Param("channel_id")) if err := h.store.SetRuleChannelEnabled(c.Request.Context(), ruleID, channelID, true); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if h.cache != nil { _ = h.cache.InvalidateChannels(c.Request.Context(), ruleID) } c.JSON(http.StatusOK, gin.H{"ok": true}) } func (h *RuleHandler) DisableChannel(c *gin.Context) { ruleID, _ := strconv.Atoi(c.Param("id")) channelID, _ := strconv.Atoi(c.Param("channel_id")) if err := h.store.SetRuleChannelEnabled(c.Request.Context(), ruleID, channelID, false); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if h.cache != nil { _ = h.cache.InvalidateChannels(c.Request.Context(), ruleID) } c.JSON(http.StatusOK, gin.H{"ok": true}) } func resolveChannelNames(s *store.Store, c *gin.Context, names []string) ([]int, error) { var ids []int for _, name := range names { ch, err := s.GetChannelByName(c.Request.Context(), name) if err != nil { return nil, err } ids = append(ids, ch.ID) } return ids, nil }