package notifications import ( "encoding/json" "fmt" "net/http" "go.uber.org/zap" "github.com/veylant/ia-gateway/internal/apierror" "github.com/veylant/ia-gateway/internal/middleware" ) // Handler handles POST /v1/notifications/send — sends a budget alert email. // Requires a valid JWT (caller must be authenticated). type Handler struct { mailer *Mailer logger *zap.Logger } // NewHandler creates a Handler. mailer must not be nil. func NewHandler(mailer *Mailer, logger *zap.Logger) *Handler { return &Handler{mailer: mailer, logger: logger} } // SendRequest is the JSON body for POST /v1/notifications/send. type SendRequest struct { To string `json:"to"` // recipient email address AlertName string `json:"alert_name"` // human-readable rule name Scope string `json:"scope"` // "global" | provider name Spend float64 `json:"spend"` // current spend for the scope Threshold float64 `json:"threshold"` // configured threshold Type string `json:"type"` // "budget_exceeded" | "budget_warning" } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { reqID := middleware.RequestIDFromContext(r.Context()) var req SendRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { apierror.WriteErrorWithRequestID(w, &apierror.APIError{ Type: "invalid_request_error", Message: "Invalid JSON body", Code: "invalid_request", HTTPStatus: http.StatusBadRequest, }, reqID) return } if req.To == "" { apierror.WriteErrorWithRequestID(w, &apierror.APIError{ Type: "invalid_request_error", Message: "Field 'to' is required", Code: "invalid_request", HTTPStatus: http.StatusBadRequest, }, reqID) return } if req.AlertName == "" { apierror.WriteErrorWithRequestID(w, &apierror.APIError{ Type: "invalid_request_error", Message: "Field 'alert_name' is required", Code: "invalid_request", HTTPStatus: http.StatusBadRequest, }, reqID) return } subject, html := buildEmail(req) if err := h.mailer.Send(Message{To: req.To, Subject: subject, HTML: html}); err != nil { h.logger.Warn("notification email send failed", zap.Error(err), zap.String("to", req.To), zap.String("alert", req.AlertName), zap.String("request_id", reqID), ) apierror.WriteErrorWithRequestID(w, &apierror.APIError{ Type: "api_error", Message: "Failed to send notification email — check SMTP configuration", Code: "email_send_failed", HTTPStatus: http.StatusServiceUnavailable, }, reqID) return } h.logger.Info("notification email sent", zap.String("to", req.To), zap.String("alert", req.AlertName), zap.String("type", req.Type), ) w.WriteHeader(http.StatusNoContent) } // buildEmail returns the subject and HTML body for a budget alert notification. func buildEmail(req SendRequest) (subject, html string) { isExceeded := req.Type == "budget_exceeded" scopeLabel := req.Scope if req.Scope == "global" { scopeLabel = "Tous fournisseurs" } pct := 0.0 if req.Threshold > 0 { pct = (req.Spend / req.Threshold) * 100 } accentColor := "#f59e0b" // warning amber statusLabel := "Alerte 80 % — seuil bientôt atteint" emoji := "⚠️" if isExceeded { accentColor = "#ef4444" // destructive red statusLabel = "Seuil dépassé" emoji = "🚨" } subject = fmt.Sprintf("[Veylant IA] %s %s : %s", emoji, statusLabel, req.AlertName) html = fmt.Sprintf(`
`, accentColor, statusLabel, req.AlertName, scopeLabel, accentColor, req.Spend, req.Threshold, accentColor, pct, accentColor, pct, pct, ) return subject, html }