Files
breakpilot-lehrer/edu-search-service/internal/api/handlers/audience_handlers.go
Benjamin Boenisch 414e0f5ec0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
feat: edu-search-service migriert, voice-service/geo-service entfernt
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor)
- opensearch + edu-search-service in docker-compose.yml hinzugefuegt
- voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core)
- geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt)
- CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt
  (Go lint, test mit go mod download, build, SBOM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:36:38 +01:00

315 lines
9.0 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/edu-search-service/internal/orchestrator"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// AudienceHandler handles audience-related HTTP requests
type AudienceHandler struct {
repo orchestrator.AudienceRepository
}
// NewAudienceHandler creates a new audience handler
func NewAudienceHandler(repo orchestrator.AudienceRepository) *AudienceHandler {
return &AudienceHandler{repo: repo}
}
// CreateAudienceRequest represents a request to create an audience
type CreateAudienceRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Filters orchestrator.AudienceFilters `json:"filters"`
CreatedBy string `json:"created_by"`
}
// UpdateAudienceRequest represents a request to update an audience
type UpdateAudienceRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Filters orchestrator.AudienceFilters `json:"filters"`
IsActive bool `json:"is_active"`
}
// CreateExportRequest represents a request to create an export
type CreateExportRequest struct {
ExportType string `json:"export_type" binding:"required"` // csv, json, email_list
Purpose string `json:"purpose"`
ExportedBy string `json:"exported_by"`
}
// ListAudiences returns all audiences
func (h *AudienceHandler) ListAudiences(c *gin.Context) {
activeOnly := c.Query("active_only") == "true"
audiences, err := h.repo.ListAudiences(c.Request.Context(), activeOnly)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list audiences", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"audiences": audiences,
"count": len(audiences),
})
}
// GetAudience returns a single audience
func (h *AudienceHandler) GetAudience(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
audience, err := h.repo.GetAudience(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Audience not found", "details": err.Error()})
return
}
c.JSON(http.StatusOK, audience)
}
// CreateAudience creates a new audience
func (h *AudienceHandler) CreateAudience(c *gin.Context) {
var req CreateAudienceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
audience := &orchestrator.Audience{
Name: req.Name,
Description: req.Description,
Filters: req.Filters,
CreatedBy: req.CreatedBy,
IsActive: true,
}
if err := h.repo.CreateAudience(c.Request.Context(), audience); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create audience", "details": err.Error()})
return
}
// Update the member count
count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID)
audience.MemberCount = count
c.JSON(http.StatusCreated, audience)
}
// UpdateAudience updates an existing audience
func (h *AudienceHandler) UpdateAudience(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
var req UpdateAudienceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
audience := &orchestrator.Audience{
ID: id,
Name: req.Name,
Description: req.Description,
Filters: req.Filters,
IsActive: req.IsActive,
}
if err := h.repo.UpdateAudience(c.Request.Context(), audience); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update audience", "details": err.Error()})
return
}
// Update the member count
count, _ := h.repo.UpdateAudienceCount(c.Request.Context(), audience.ID)
audience.MemberCount = count
c.JSON(http.StatusOK, audience)
}
// DeleteAudience soft-deletes an audience
func (h *AudienceHandler) DeleteAudience(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
if err := h.repo.DeleteAudience(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete audience", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"deleted": true, "id": idStr})
}
// GetAudienceMembers returns members matching the audience filters
func (h *AudienceHandler) GetAudienceMembers(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
// Parse pagination
limit := 50
offset := 0
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 && parsed <= 500 {
limit = parsed
}
}
if o := c.Query("offset"); o != "" {
if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 {
offset = parsed
}
}
members, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"members": members,
"count": len(members),
"total_count": totalCount,
"limit": limit,
"offset": offset,
})
}
// RefreshAudienceCount recalculates the member count
func (h *AudienceHandler) RefreshAudienceCount(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
count, err := h.repo.UpdateAudienceCount(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh count", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"audience_id": idStr,
"member_count": count,
})
}
// PreviewAudienceFilters previews the result of filters without saving
func (h *AudienceHandler) PreviewAudienceFilters(c *gin.Context) {
var filters orchestrator.AudienceFilters
if err := c.ShouldBindJSON(&filters); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
// Return the filters for now - preview functionality can be expanded later
c.JSON(http.StatusOK, gin.H{
"filters": filters,
"message": "Preview functionality requires direct repository access",
})
}
// CreateExport creates a new export for an audience
func (h *AudienceHandler) CreateExport(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
var req CreateExportRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
// Get the member count for the export
_, totalCount, err := h.repo.GetAudienceMembers(c.Request.Context(), id, 1, 0)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get members", "details": err.Error()})
return
}
export := &orchestrator.AudienceExport{
AudienceID: id,
ExportType: req.ExportType,
RecordCount: totalCount,
ExportedBy: req.ExportedBy,
Purpose: req.Purpose,
}
if err := h.repo.CreateExport(c.Request.Context(), export); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export", "details": err.Error()})
return
}
c.JSON(http.StatusCreated, export)
}
// ListExports lists exports for an audience
func (h *AudienceHandler) ListExports(c *gin.Context) {
idStr := c.Param("id")
id, err := uuid.Parse(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid audience ID"})
return
}
exports, err := h.repo.ListExports(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list exports", "details": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"exports": exports,
"count": len(exports),
})
}
// SetupAudienceRoutes configures audience API routes
func SetupAudienceRoutes(r *gin.RouterGroup, h *AudienceHandler) {
audiences := r.Group("/audiences")
{
// Audience CRUD
audiences.GET("", h.ListAudiences)
audiences.GET("/:id", h.GetAudience)
audiences.POST("", h.CreateAudience)
audiences.PUT("/:id", h.UpdateAudience)
audiences.DELETE("/:id", h.DeleteAudience)
// Members
audiences.GET("/:id/members", h.GetAudienceMembers)
audiences.POST("/:id/refresh", h.RefreshAudienceCount)
// Exports
audiences.GET("/:id/exports", h.ListExports)
audiences.POST("/:id/exports", h.CreateExport)
// Preview (no audience required)
audiences.POST("/preview", h.PreviewAudienceFilters)
}
}