Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC. No exported symbols renamed; zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
217 lines
7.1 KiB
Go
217 lines
7.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
|
)
|
|
|
|
// ExportMemo exports the obligations overview as a C-Level memo
|
|
// POST /sdk/v1/ucca/obligations/export/memo
|
|
func (h *ObligationsHandlers) ExportMemo(c *gin.Context) {
|
|
tenantID := rbac.GetTenantID(c)
|
|
|
|
var req ucca.ExportMemoRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
if h.store == nil {
|
|
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
|
|
return
|
|
}
|
|
|
|
id, err := uuid.Parse(req.AssessmentID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
|
|
return
|
|
}
|
|
|
|
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
|
|
return
|
|
}
|
|
|
|
exporter := ucca.NewPDFExporter(req.Language)
|
|
|
|
var response *ucca.ExportMemoResponse
|
|
switch req.Format {
|
|
case "pdf":
|
|
response, err = exporter.ExportManagementMemo(assessment.Overview)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF", "details": err.Error()})
|
|
return
|
|
}
|
|
case "markdown", "":
|
|
response, err = exporter.ExportMarkdown(assessment.Overview)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Markdown", "details": err.Error()})
|
|
return
|
|
}
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid format. Use 'markdown' or 'pdf'"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// ExportMemoFromOverview exports an overview directly (without persistence)
|
|
// POST /sdk/v1/ucca/obligations/export/direct
|
|
func (h *ObligationsHandlers) ExportMemoFromOverview(c *gin.Context) {
|
|
var req struct {
|
|
Overview *ucca.ManagementObligationsOverview `json:"overview"`
|
|
Format string `json:"format"` // "markdown" or "pdf"
|
|
Language string `json:"language,omitempty"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
if req.Overview == nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Overview is required"})
|
|
return
|
|
}
|
|
|
|
exporter := ucca.NewPDFExporter(req.Language)
|
|
|
|
var response *ucca.ExportMemoResponse
|
|
var err error
|
|
switch req.Format {
|
|
case "pdf":
|
|
response, err = exporter.ExportManagementMemo(req.Overview)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF", "details": err.Error()})
|
|
return
|
|
}
|
|
case "markdown", "":
|
|
response, err = exporter.ExportMarkdown(req.Overview)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Markdown", "details": err.Error()})
|
|
return
|
|
}
|
|
default:
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid format. Use 'markdown' or 'pdf'"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func generateMemoMarkdown(overview *ucca.ManagementObligationsOverview) string {
|
|
content := "# Pflichten-Übersicht für die Geschäftsführung\n\n"
|
|
content += "**Datum:** " + overview.AssessmentDate.Format("02.01.2006") + "\n"
|
|
if overview.OrganizationName != "" {
|
|
content += "**Organisation:** " + overview.OrganizationName + "\n"
|
|
}
|
|
content += "\n---\n\n"
|
|
|
|
content += "## Executive Summary\n\n"
|
|
content += "| Kennzahl | Wert |\n"
|
|
content += "|----------|------|\n"
|
|
content += "| Anwendbare Regulierungen | " + itoa(overview.ExecutiveSummary.TotalRegulations) + " |\n"
|
|
content += "| Gesamtzahl Pflichten | " + itoa(overview.ExecutiveSummary.TotalObligations) + " |\n"
|
|
content += "| Kritische Pflichten | " + itoa(overview.ExecutiveSummary.CriticalObligations) + " |\n"
|
|
content += "| Überfällige Pflichten | " + itoa(overview.ExecutiveSummary.OverdueObligations) + " |\n"
|
|
content += "| Anstehende Fristen (30 Tage) | " + itoa(overview.ExecutiveSummary.UpcomingDeadlines) + " |\n"
|
|
content += "\n"
|
|
|
|
if len(overview.ExecutiveSummary.KeyRisks) > 0 {
|
|
content += "### Hauptrisiken\n\n"
|
|
for _, risk := range overview.ExecutiveSummary.KeyRisks {
|
|
content += "- ⚠️ " + risk + "\n"
|
|
}
|
|
content += "\n"
|
|
}
|
|
|
|
if len(overview.ExecutiveSummary.RecommendedActions) > 0 {
|
|
content += "### Empfohlene Maßnahmen\n\n"
|
|
for i, action := range overview.ExecutiveSummary.RecommendedActions {
|
|
content += itoa(i+1) + ". " + action + "\n"
|
|
}
|
|
content += "\n"
|
|
}
|
|
|
|
content += "## Anwendbare Regulierungen\n\n"
|
|
for _, reg := range overview.ApplicableRegulations {
|
|
content += "### " + reg.Name + "\n\n"
|
|
content += "- **Klassifizierung:** " + reg.Classification + "\n"
|
|
content += "- **Begründung:** " + reg.Reason + "\n"
|
|
content += "- **Anzahl Pflichten:** " + itoa(reg.ObligationCount) + "\n"
|
|
content += "\n"
|
|
}
|
|
|
|
content += "## Sanktionsrisiken\n\n"
|
|
content += overview.SanctionsSummary.Summary + "\n\n"
|
|
if overview.SanctionsSummary.MaxFinancialRisk != "" {
|
|
content += "- **Maximales Bußgeld:** " + overview.SanctionsSummary.MaxFinancialRisk + "\n"
|
|
}
|
|
if overview.SanctionsSummary.PersonalLiabilityRisk {
|
|
content += "- **Persönliche Haftung:** Ja ⚠️\n"
|
|
}
|
|
content += "\n"
|
|
|
|
content += "## Kritische Pflichten\n\n"
|
|
for _, obl := range overview.Obligations {
|
|
if obl.Priority == ucca.PriorityCritical {
|
|
content += "### " + obl.ID + ": " + obl.Title + "\n\n"
|
|
content += obl.Description + "\n\n"
|
|
content += "- **Verantwortlich:** " + string(obl.Responsible) + "\n"
|
|
if obl.Deadline != nil {
|
|
if obl.Deadline.Date != nil {
|
|
content += "- **Frist:** " + obl.Deadline.Date.Format("02.01.2006") + "\n"
|
|
} else if obl.Deadline.Duration != "" {
|
|
content += "- **Frist:** " + obl.Deadline.Duration + "\n"
|
|
}
|
|
}
|
|
if obl.Sanctions != nil && obl.Sanctions.MaxFine != "" {
|
|
content += "- **Sanktion:** " + obl.Sanctions.MaxFine + "\n"
|
|
}
|
|
content += "\n"
|
|
}
|
|
}
|
|
|
|
if len(overview.IncidentDeadlines) > 0 {
|
|
content += "## Meldepflichten bei Sicherheitsvorfällen\n\n"
|
|
content += "| Phase | Frist | Empfänger |\n"
|
|
content += "|-------|-------|-----------|\n"
|
|
for _, deadline := range overview.IncidentDeadlines {
|
|
content += "| " + deadline.Phase + " | " + deadline.Deadline + " | " + deadline.Recipient + " |\n"
|
|
}
|
|
content += "\n"
|
|
}
|
|
|
|
content += "---\n\n"
|
|
content += "*Dieses Dokument wurde automatisch generiert und ersetzt keine Rechtsberatung.*\n"
|
|
|
|
return content
|
|
}
|
|
|
|
func isEUCountry(country string) bool {
|
|
euCountries := map[string]bool{
|
|
"DE": true, "AT": true, "BE": true, "BG": true, "HR": true, "CY": true,
|
|
"CZ": true, "DK": true, "EE": true, "FI": true, "FR": true, "GR": true,
|
|
"HU": true, "IE": true, "IT": true, "LV": true, "LT": true, "LU": true,
|
|
"MT": true, "NL": true, "PL": true, "PT": true, "RO": true, "SK": true,
|
|
"SI": true, "ES": true, "SE": true,
|
|
}
|
|
return euCountries[country]
|
|
}
|
|
|
|
func itoa(i int) string {
|
|
return strconv.Itoa(i)
|
|
}
|