This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +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)
}
}