Files
breakpilot-compliance/ai-compliance-sdk/internal/ucca/pdf_export.go
Sharang Parnerkar f7a5f9e1ed refactor(go/ucca): split license_policy, models, pdf_export, escalation_store, obligations_registry
Split 5 oversized files (501-583 LOC each) into focused units all under 500 LOC:
- license_policy.go → +_types.go (engine logic / type definitions)
- models.go → +_intake.go, +_assessment.go (enums+domains / intake structs / output+DB types)
- pdf_export.go → +_markdown.go (PDF export / markdown export)
- escalation_store.go → +_dsb.go (main escalation ops / DSB pool ops)
- obligations_registry.go → +_grouping.go (registry core / grouping methods)

All files remain in package ucca. Zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:03:51 +02:00

411 lines
12 KiB
Go

package ucca
import (
"bytes"
"encoding/base64"
"fmt"
"time"
"github.com/jung-kurt/gofpdf"
)
// PDFExporter generates PDF documents from obligations assessments
type PDFExporter struct {
language string
}
// NewPDFExporter creates a new PDF exporter
func NewPDFExporter(language string) *PDFExporter {
if language == "" {
language = "de"
}
return &PDFExporter{language: language}
}
// ExportManagementMemo exports the management obligations overview as a PDF
func (e *PDFExporter) ExportManagementMemo(overview *ManagementObligationsOverview) (*ExportMemoResponse, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
// Set UTF-8 support with DejaVu font (fallback to core fonts)
pdf.SetFont("Helvetica", "", 12)
// Add first page
pdf.AddPage()
// Add title
e.addTitle(pdf, overview)
// Add executive summary
e.addExecutiveSummary(pdf, overview)
// Add applicable regulations
e.addApplicableRegulations(pdf, overview)
// Add sanctions summary
e.addSanctionsSummary(pdf, overview)
// Add obligations table
e.addObligationsTable(pdf, overview)
// Add incident deadlines if present
if len(overview.IncidentDeadlines) > 0 {
e.addIncidentDeadlines(pdf, overview)
}
// Add footer with generation date
e.addFooter(pdf, overview)
// Generate PDF bytes
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, fmt.Errorf("failed to generate PDF: %w", err)
}
// Encode as base64
content := base64.StdEncoding.EncodeToString(buf.Bytes())
return &ExportMemoResponse{
Content: content,
ContentType: "application/pdf",
Filename: fmt.Sprintf("pflichten-uebersicht-%s.pdf", time.Now().Format("2006-01-02")),
GeneratedAt: time.Now(),
}, nil
}
// addTitle adds the document title
func (e *PDFExporter) addTitle(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
pdf.SetFont("Helvetica", "B", 24)
pdf.SetTextColor(0, 0, 0)
title := "Regulatorische Pflichten-Uebersicht"
if e.language == "en" {
title = "Regulatory Obligations Overview"
}
pdf.CellFormat(0, 15, title, "", 1, "C", false, 0, "")
// Organization name
if overview.OrganizationName != "" {
pdf.SetFont("Helvetica", "", 14)
pdf.CellFormat(0, 10, overview.OrganizationName, "", 1, "C", false, 0, "")
}
// Date
pdf.SetFont("Helvetica", "I", 10)
dateStr := overview.AssessmentDate.Format("02.01.2006")
pdf.CellFormat(0, 8, fmt.Sprintf("Stand: %s", dateStr), "", 1, "C", false, 0, "")
pdf.Ln(10)
}
// addExecutiveSummary adds the executive summary section
func (e *PDFExporter) addExecutiveSummary(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
e.addSectionHeader(pdf, "Executive Summary")
summary := overview.ExecutiveSummary
// Stats table
pdf.SetFont("Helvetica", "", 11)
stats := []struct {
label string
value string
}{
{"Anwendbare Regulierungen", fmt.Sprintf("%d", summary.TotalRegulations)},
{"Gesamtzahl Pflichten", fmt.Sprintf("%d", summary.TotalObligations)},
{"Kritische Pflichten", fmt.Sprintf("%d", summary.CriticalObligations)},
{"Kommende Fristen (30 Tage)", fmt.Sprintf("%d", summary.UpcomingDeadlines)},
{"Compliance Score", fmt.Sprintf("%d%%", summary.ComplianceScore)},
}
for _, stat := range stats {
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(80, 7, stat.label+":", "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 11)
pdf.CellFormat(0, 7, stat.value, "", 1, "L", false, 0, "")
}
// Key risks
if len(summary.KeyRisks) > 0 {
pdf.Ln(5)
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(0, 7, "Wesentliche Risiken:", "", 1, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 10)
for _, risk := range summary.KeyRisks {
pdf.CellFormat(10, 6, "", "", 0, "L", false, 0, "")
pdf.CellFormat(0, 6, "- "+risk, "", 1, "L", false, 0, "")
}
}
// Recommended actions
if len(summary.RecommendedActions) > 0 {
pdf.Ln(5)
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(0, 7, "Empfohlene Massnahmen:", "", 1, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 10)
for _, action := range summary.RecommendedActions {
pdf.CellFormat(10, 6, "", "", 0, "L", false, 0, "")
pdf.CellFormat(0, 6, "- "+action, "", 1, "L", false, 0, "")
}
}
pdf.Ln(10)
}
// addApplicableRegulations adds the applicable regulations section
func (e *PDFExporter) addApplicableRegulations(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
e.addSectionHeader(pdf, "Anwendbare Regulierungen")
pdf.SetFont("Helvetica", "", 10)
// Table header
pdf.SetFillColor(240, 240, 240)
pdf.SetFont("Helvetica", "B", 10)
pdf.CellFormat(60, 8, "Regulierung", "1", 0, "L", true, 0, "")
pdf.CellFormat(50, 8, "Klassifizierung", "1", 0, "L", true, 0, "")
pdf.CellFormat(30, 8, "Pflichten", "1", 0, "C", true, 0, "")
pdf.CellFormat(50, 8, "Grund", "1", 1, "L", true, 0, "")
pdf.SetFont("Helvetica", "", 10)
for _, reg := range overview.ApplicableRegulations {
pdf.CellFormat(60, 7, reg.Name, "1", 0, "L", false, 0, "")
pdf.CellFormat(50, 7, truncateString(reg.Classification, 25), "1", 0, "L", false, 0, "")
pdf.CellFormat(30, 7, fmt.Sprintf("%d", reg.ObligationCount), "1", 0, "C", false, 0, "")
pdf.CellFormat(50, 7, truncateString(reg.Reason, 25), "1", 1, "L", false, 0, "")
}
pdf.Ln(10)
}
// addSanctionsSummary adds the sanctions summary section
func (e *PDFExporter) addSanctionsSummary(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
e.addSectionHeader(pdf, "Sanktionsrisiken")
sanctions := overview.SanctionsSummary
pdf.SetFont("Helvetica", "", 11)
// Max financial risk
if sanctions.MaxFinancialRisk != "" {
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(60, 7, "Max. Finanzrisiko:", "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 11)
pdf.SetTextColor(200, 0, 0)
pdf.CellFormat(0, 7, sanctions.MaxFinancialRisk, "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
}
// Personal liability
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(60, 7, "Persoenliche Haftung:", "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 11)
liabilityText := "Nein"
if sanctions.PersonalLiabilityRisk {
liabilityText = "Ja - Geschaeftsfuehrung kann persoenlich haften"
pdf.SetTextColor(200, 0, 0)
}
pdf.CellFormat(0, 7, liabilityText, "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
// Criminal liability
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(60, 7, "Strafrechtliche Konsequenzen:", "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 11)
criminalText := "Nein"
if sanctions.CriminalLiabilityRisk {
criminalText = "Moeglich"
pdf.SetTextColor(200, 0, 0)
}
pdf.CellFormat(0, 7, criminalText, "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
// Summary
if sanctions.Summary != "" {
pdf.Ln(3)
pdf.SetFont("Helvetica", "I", 10)
pdf.MultiCell(0, 5, sanctions.Summary, "", "L", false)
}
pdf.Ln(10)
}
// addObligationsTable adds the obligations table
func (e *PDFExporter) addObligationsTable(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
e.addSectionHeader(pdf, "Pflichten-Uebersicht")
// Group by priority
criticalObls := []Obligation{}
highObls := []Obligation{}
otherObls := []Obligation{}
for _, obl := range overview.Obligations {
switch obl.Priority {
case PriorityCritical, ObligationPriority("kritisch"):
criticalObls = append(criticalObls, obl)
case PriorityHigh, ObligationPriority("hoch"):
highObls = append(highObls, obl)
default:
otherObls = append(otherObls, obl)
}
}
// Critical obligations
if len(criticalObls) > 0 {
pdf.SetFont("Helvetica", "B", 11)
pdf.SetTextColor(200, 0, 0)
pdf.CellFormat(0, 8, fmt.Sprintf("Kritische Pflichten (%d)", len(criticalObls)), "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
e.addObligationsList(pdf, criticalObls)
}
// High priority obligations
if len(highObls) > 0 {
pdf.SetFont("Helvetica", "B", 11)
pdf.SetTextColor(200, 100, 0)
pdf.CellFormat(0, 8, fmt.Sprintf("Hohe Prioritaet (%d)", len(highObls)), "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
e.addObligationsList(pdf, highObls)
}
// Other obligations
if len(otherObls) > 0 {
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(0, 8, fmt.Sprintf("Weitere Pflichten (%d)", len(otherObls)), "", 1, "L", false, 0, "")
e.addObligationsList(pdf, otherObls)
}
}
// addObligationsList adds a list of obligations
func (e *PDFExporter) addObligationsList(pdf *gofpdf.Fpdf, obligations []Obligation) {
// Check if we need a new page
if pdf.GetY() > 250 {
pdf.AddPage()
}
pdf.SetFont("Helvetica", "", 9)
for _, obl := range obligations {
// Check if we need a new page
if pdf.GetY() > 270 {
pdf.AddPage()
}
// Obligation ID and title
pdf.SetFont("Helvetica", "B", 9)
pdf.CellFormat(25, 5, obl.ID, "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 9)
pdf.CellFormat(0, 5, truncateString(obl.Title, 80), "", 1, "L", false, 0, "")
// Legal basis
if len(obl.LegalBasis) > 0 {
pdf.SetFont("Helvetica", "I", 8)
pdf.CellFormat(25, 4, "", "", 0, "L", false, 0, "")
legalText := ""
for i, lb := range obl.LegalBasis {
if i > 0 {
legalText += ", "
}
legalText += lb.Norm
}
pdf.CellFormat(0, 4, truncateString(legalText, 100), "", 1, "L", false, 0, "")
}
// Deadline
if obl.Deadline != nil {
pdf.SetFont("Helvetica", "", 8)
pdf.CellFormat(25, 4, "", "", 0, "L", false, 0, "")
deadlineText := "Frist: "
if obl.Deadline.Date != nil {
deadlineText += obl.Deadline.Date.Format("02.01.2006")
} else if obl.Deadline.Duration != "" {
deadlineText += obl.Deadline.Duration
} else if obl.Deadline.Interval != "" {
deadlineText += obl.Deadline.Interval
}
pdf.CellFormat(0, 4, deadlineText, "", 1, "L", false, 0, "")
}
// Responsible
pdf.SetFont("Helvetica", "", 8)
pdf.CellFormat(25, 4, "", "", 0, "L", false, 0, "")
pdf.CellFormat(0, 4, fmt.Sprintf("Verantwortlich: %s", obl.Responsible), "", 1, "L", false, 0, "")
pdf.Ln(2)
}
pdf.Ln(5)
}
// addIncidentDeadlines adds the incident deadlines section
func (e *PDFExporter) addIncidentDeadlines(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
// Check if we need a new page
if pdf.GetY() > 220 {
pdf.AddPage()
}
e.addSectionHeader(pdf, "Meldepflichten bei Vorfaellen")
pdf.SetFont("Helvetica", "", 10)
// Group by regulation
byRegulation := make(map[string][]IncidentDeadline)
for _, deadline := range overview.IncidentDeadlines {
byRegulation[deadline.RegulationID] = append(byRegulation[deadline.RegulationID], deadline)
}
for regID, deadlines := range byRegulation {
pdf.SetFont("Helvetica", "B", 10)
regName := regID
for _, reg := range overview.ApplicableRegulations {
if reg.ID == regID {
regName = reg.Name
break
}
}
pdf.CellFormat(0, 7, regName, "", 1, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 9)
for _, dl := range deadlines {
pdf.CellFormat(40, 6, dl.Phase+":", "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "B", 9)
pdf.CellFormat(30, 6, dl.Deadline, "", 0, "L", false, 0, "")
pdf.SetFont("Helvetica", "", 9)
pdf.CellFormat(0, 6, "an "+dl.Recipient, "", 1, "L", false, 0, "")
}
pdf.Ln(3)
}
pdf.Ln(5)
}
// addFooter adds the document footer
func (e *PDFExporter) addFooter(pdf *gofpdf.Fpdf, overview *ManagementObligationsOverview) {
pdf.SetY(-30)
pdf.SetFont("Helvetica", "I", 8)
pdf.SetTextColor(128, 128, 128)
pdf.CellFormat(0, 5, fmt.Sprintf("Generiert am %s mit BreakPilot AI Compliance SDK", time.Now().Format("02.01.2006 15:04")), "", 1, "C", false, 0, "")
pdf.CellFormat(0, 5, "Dieses Dokument ersetzt keine rechtliche Beratung.", "", 1, "C", false, 0, "")
}
// addSectionHeader adds a section header
func (e *PDFExporter) addSectionHeader(pdf *gofpdf.Fpdf, title string) {
pdf.SetFont("Helvetica", "B", 14)
pdf.SetTextColor(50, 50, 50)
pdf.CellFormat(0, 10, title, "", 1, "L", false, 0, "")
pdf.SetTextColor(0, 0, 0)
// Underline
pdf.SetDrawColor(200, 200, 200)
pdf.Line(10, pdf.GetY(), 200, pdf.GetY())
pdf.Ln(5)
}
// truncateString truncates a string to maxLen characters
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}