This commit is contained in:
Tomáš Dvořák
2025-10-16 13:32:05 +02:00
commit 12cba639b9
663 changed files with 168914 additions and 0 deletions
@@ -0,0 +1,158 @@
package controllers
import (
"net/http"
"strings"
"fotbal-club/internal/models"
"fotbal-club/pkg/email"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// NotificationsController handles admin-triggered notifications (emails)
// It reuses the newsletter sending pipeline/templates.
type NotificationsController struct {
DB *gorm.DB
EmailService email.EmailService
}
func NewNotificationsController(db *gorm.DB, emailService email.EmailService) *NotificationsController {
return &NotificationsController{DB: db, EmailService: emailService}
}
// Common request payload for notifications
// Subject is required; body/content carries HTML. Recipients optional.
// If SendToSubscribers is true, all active newsletter subscribers will be included.
// Optional context identifiers (competition code or match ext ID) are accepted for logging/auditing
// or future template specialization, but are not required for sending right now.
type NotificationRequest struct {
Subject string `json:"subject" binding:"required"`
Body string `json:"body"`
Content string `json:"content"`
Recipients []string `json:"recipients"`
SendToSubscribers bool `json:"send_to_subscribers"`
CompetitionCode string `json:"competition_code"`
MatchExternalID string `json:"match_external_id"`
}
// SendCompetitionNotification sends a notification related to a competition
// @Summary Send competition notification (admin)
// @Description Sends a notification email for a competition to selected recipients and/or newsletter subscribers
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param input body NotificationRequest true "Notification payload"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/notifications/competition [post]
func (nc *NotificationsController) SendCompetitionNotification(c *gin.Context) {
nc.sendNotification(c)
}
// SendMatchNotification sends a notification related to a match
// @Summary Send match notification (admin)
// @Description Sends a notification email for a match to selected recipients and/or newsletter subscribers
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param input body NotificationRequest true "Notification payload"
// @Success 200 {object} map[string]any
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/notifications/match [post]
func (nc *NotificationsController) SendMatchNotification(c *gin.Context) {
nc.sendNotification(c)
}
// Internal shared implementation
func (nc *NotificationsController) sendNotification(c *gin.Context) {
// Only admins
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input NotificationRequest
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
// Prefer Body then Content
body := strings.TrimSpace(input.Body)
if body == "" {
body = strings.TrimSpace(input.Content)
}
if input.Subject == "" || body == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "subject and body/content are required"})
return
}
// Build recipients
recipients := make([]string, 0, len(input.Recipients)+16)
// Explicit recipients
for _, r := range input.Recipients {
r = strings.TrimSpace(strings.ToLower(r))
if r != "" {
recipients = append(recipients, r)
}
}
// Include subscribers if requested
if input.SendToSubscribers {
var subs []models.NewsletterSubscription
if err := nc.DB.Where("is_active = ?", true).Find(&subs).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load subscribers"})
return
}
for _, s := range subs {
if s.Email != "" {
recipients = append(recipients, strings.ToLower(strings.TrimSpace(s.Email)))
}
}
}
// Dedupe recipients
uniq := make(map[string]struct{}, len(recipients))
out := make([]string, 0, len(recipients))
for _, r := range recipients {
if r == "" {
continue
}
if _, exists := uniq[r]; !exists {
uniq[r] = struct{}{}
out = append(out, r)
}
}
if len(out) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No recipients resolved"})
return
}
// Send using newsletter template/pipeline
data := &email.NewsletterData{
Subject: input.Subject,
Content: body,
Recipients: out,
}
if err := nc.EmailService.SendNewsletter(data); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send notifications"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Notifications sent",
"recipients": len(out),
"competition": input.CompetitionCode,
"match": input.MatchExternalID,
})
}