Files
breakpilot-compliance/ai-compliance-sdk/internal/funding/export.go
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

396 lines
12 KiB
Go

package funding
import (
"archive/zip"
"bytes"
"fmt"
"io"
"time"
"github.com/jung-kurt/gofpdf"
"github.com/xuri/excelize/v2"
)
// ExportService handles document generation
type ExportService struct{}
// NewExportService creates a new export service
func NewExportService() *ExportService {
return &ExportService{}
}
// GenerateApplicationLetter generates the main application letter as PDF
func (s *ExportService) GenerateApplicationLetter(app *FundingApplication) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(25, 25, 25)
pdf.AddPage()
// Header
pdf.SetFont("Helvetica", "B", 14)
pdf.Cell(0, 10, "Antrag auf Foerderung im Rahmen der digitalen Bildungsinfrastruktur")
pdf.Ln(15)
// Application number
pdf.SetFont("Helvetica", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Antragsnummer: %s", app.ApplicationNumber))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Datum: %s", time.Now().Format("02.01.2006")))
pdf.Ln(15)
// Section 1: Einleitung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "1. Einleitung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.SchoolProfile != nil {
pdf.MultiCell(0, 6, fmt.Sprintf(
"Die %s (Schulnummer: %s) beantragt hiermit Foerdermittel aus dem Programm %s.\n\n"+
"Schultraeger: %s\n"+
"Schulform: %s\n"+
"Schueleranzahl: %d\n"+
"Lehrkraefte: %d",
app.SchoolProfile.Name,
app.SchoolProfile.SchoolNumber,
app.FundingProgram,
app.SchoolProfile.CarrierName,
app.SchoolProfile.Type,
app.SchoolProfile.StudentCount,
app.SchoolProfile.TeacherCount,
), "", "", false)
}
pdf.Ln(10)
// Section 2: Projektziel
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "2. Projektziel")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil {
pdf.MultiCell(0, 6, app.ProjectPlan.Summary, "", "", false)
pdf.Ln(5)
pdf.MultiCell(0, 6, app.ProjectPlan.Goals, "", "", false)
}
pdf.Ln(10)
// Section 3: Beschreibung der Massnahme
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "3. Beschreibung der Massnahme")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil {
pdf.MultiCell(0, 6, app.ProjectPlan.DidacticConcept, "", "", false)
}
pdf.Ln(10)
// Section 4: Datenschutz & IT-Betrieb
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "4. Datenschutz & IT-Betrieb")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.DataProtection != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.DataProtection, "", "", false)
}
pdf.Ln(10)
// Section 5: Kosten & Finanzierung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "5. Kosten & Finanzierung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.Budget != nil {
pdf.Cell(0, 6, fmt.Sprintf("Gesamtkosten: %.2f EUR", app.Budget.TotalCost))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Beantragter Foerderbetrag: %.2f EUR (%.0f%%)", app.Budget.RequestedFunding, app.Budget.FundingRate*100))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Eigenanteil: %.2f EUR", app.Budget.OwnContribution))
}
pdf.Ln(10)
// Section 6: Laufzeit
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "6. Laufzeit")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.Timeline != nil {
pdf.Cell(0, 6, fmt.Sprintf("Projektbeginn: %s", app.Timeline.PlannedStart.Format("02.01.2006")))
pdf.Ln(6)
pdf.Cell(0, 6, fmt.Sprintf("Projektende: %s", app.Timeline.PlannedEnd.Format("02.01.2006")))
}
pdf.Ln(15)
// Footer note
pdf.SetFont("Helvetica", "I", 9)
pdf.MultiCell(0, 5, "Hinweis: Dieser Antrag wurde mit dem Foerderantrag-Wizard von BreakPilot erstellt. "+
"Die finale Pruefung und Einreichung erfolgt durch den Schultraeger.", "", "", false)
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateBudgetPlan generates the budget plan as XLSX
func (s *ExportService) GenerateBudgetPlan(app *FundingApplication) ([]byte, error) {
f := excelize.NewFile()
sheetName := "Kostenplan"
f.SetSheetName("Sheet1", sheetName)
// Header row
headers := []string{
"Pos.", "Kategorie", "Beschreibung", "Hersteller",
"Anzahl", "Einzelpreis", "Gesamt", "Foerderfahig", "Finanzierung",
}
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheetName, cell, h)
}
// Style header
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
Fill: excelize.Fill{Type: "pattern", Color: []string{"#E0E0E0"}, Pattern: 1},
})
f.SetRowStyle(sheetName, 1, 1, headerStyle)
// Data rows
row := 2
if app.Budget != nil {
for i, item := range app.Budget.BudgetItems {
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), i+1)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), string(item.Category))
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), item.Description)
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), item.Manufacturer)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), item.Quantity)
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), item.UnitPrice)
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), item.TotalPrice)
fundable := "Nein"
if item.IsFundable {
fundable = "Ja"
}
f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), fundable)
f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), item.FundingSource)
row++
}
// Summary rows
row += 2
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Gesamtkosten:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.TotalCost)
row++
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Foerderbetrag:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.RequestedFunding)
row++
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), "Eigenanteil:")
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), app.Budget.OwnContribution)
}
// Set column widths
f.SetColWidth(sheetName, "A", "A", 6)
f.SetColWidth(sheetName, "B", "B", 15)
f.SetColWidth(sheetName, "C", "C", 35)
f.SetColWidth(sheetName, "D", "D", 15)
f.SetColWidth(sheetName, "E", "E", 8)
f.SetColWidth(sheetName, "F", "F", 12)
f.SetColWidth(sheetName, "G", "G", 12)
f.SetColWidth(sheetName, "H", "H", 12)
f.SetColWidth(sheetName, "I", "I", 15)
// Add currency format
currencyStyle, _ := f.NewStyle(&excelize.Style{
NumFmt: 44, // Currency format
})
f.SetColStyle(sheetName, "F", currencyStyle)
f.SetColStyle(sheetName, "G", currencyStyle)
var buf bytes.Buffer
if err := f.Write(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateDataProtectionConcept generates the data protection concept as PDF
func (s *ExportService) GenerateDataProtectionConcept(app *FundingApplication) ([]byte, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(25, 25, 25)
pdf.AddPage()
// Header
pdf.SetFont("Helvetica", "B", 14)
pdf.Cell(0, 10, "Datenschutz- und Betriebskonzept")
pdf.Ln(15)
pdf.SetFont("Helvetica", "", 10)
pdf.Cell(0, 6, fmt.Sprintf("Antragsnummer: %s", app.ApplicationNumber))
pdf.Ln(6)
if app.SchoolProfile != nil {
pdf.Cell(0, 6, fmt.Sprintf("Schule: %s", app.SchoolProfile.Name))
}
pdf.Ln(15)
// Section: Lokale Verarbeitung
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "1. Grundsaetze der Datenverarbeitung")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.DataProtection != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.DataProtection, "", "", false)
} else {
pdf.MultiCell(0, 6, "Das Projekt setzt auf eine vollstaendig lokale Datenverarbeitung:\n\n"+
"- Alle Daten werden ausschliesslich auf den schuleigenen Systemen verarbeitet\n"+
"- Keine Uebermittlung personenbezogener Daten an externe Dienste\n"+
"- Keine Cloud-Speicherung sensibler Daten\n"+
"- Betrieb im Verantwortungsbereich der Schule", "", "", false)
}
pdf.Ln(10)
// Section: Technische Massnahmen
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "2. Technische und organisatorische Massnahmen")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
pdf.MultiCell(0, 6, "Folgende TOMs werden umgesetzt:\n\n"+
"- Zugriffskontrolle ueber schuleigene Benutzerverwaltung\n"+
"- Verschluesselte Datenspeicherung\n"+
"- Regelmaessige Sicherheitsupdates\n"+
"- Protokollierung von Zugriffen\n"+
"- Automatische Loeschung nach definierten Fristen", "", "", false)
pdf.Ln(10)
// Section: Betriebskonzept
pdf.SetFont("Helvetica", "B", 12)
pdf.Cell(0, 8, "3. Betriebskonzept")
pdf.Ln(10)
pdf.SetFont("Helvetica", "", 10)
if app.ProjectPlan != nil && app.ProjectPlan.MaintenancePlan != "" {
pdf.MultiCell(0, 6, app.ProjectPlan.MaintenancePlan, "", "", false)
} else {
pdf.MultiCell(0, 6, "Der laufende Betrieb wird wie folgt sichergestellt:\n\n"+
"- Schulung des technischen Personals\n"+
"- Dokumentierte Betriebsverfahren\n"+
"- Regelmaessige Wartung und Updates\n"+
"- Definierte Ansprechpartner", "", "", false)
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GenerateExportBundle generates a ZIP file with all documents
func (s *ExportService) GenerateExportBundle(app *FundingApplication) ([]byte, error) {
var buf bytes.Buffer
zipWriter := zip.NewWriter(&buf)
// Generate and add application letter
letter, err := s.GenerateApplicationLetter(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Antragsschreiben.pdf", app.ApplicationNumber))
w.Write(letter)
}
// Generate and add budget plan
budget, err := s.GenerateBudgetPlan(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Kostenplan.xlsx", app.ApplicationNumber))
w.Write(budget)
}
// Generate and add data protection concept
dp, err := s.GenerateDataProtectionConcept(app)
if err == nil {
w, _ := zipWriter.Create(fmt.Sprintf("%s_Datenschutzkonzept.pdf", app.ApplicationNumber))
w.Write(dp)
}
// Add attachments
for _, attachment := range app.Attachments {
// Read attachment from storage and add to ZIP
// This would need actual file system access
_ = attachment
}
if err := zipWriter.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// ExportDocument represents a generated document
type GeneratedDocument struct {
Name string
Type string // pdf, xlsx, docx
Content []byte
MimeType string
}
// GenerateAllDocuments generates all documents for an application
func (s *ExportService) GenerateAllDocuments(app *FundingApplication) ([]GeneratedDocument, error) {
var docs []GeneratedDocument
// Application letter
letter, err := s.GenerateApplicationLetter(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Antragsschreiben.pdf", app.ApplicationNumber),
Type: "pdf",
Content: letter,
MimeType: "application/pdf",
})
}
// Budget plan
budget, err := s.GenerateBudgetPlan(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Kostenplan.xlsx", app.ApplicationNumber),
Type: "xlsx",
Content: budget,
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
}
// Data protection concept
dp, err := s.GenerateDataProtectionConcept(app)
if err == nil {
docs = append(docs, GeneratedDocument{
Name: fmt.Sprintf("%s_Datenschutzkonzept.pdf", app.ApplicationNumber),
Type: "pdf",
Content: dp,
MimeType: "application/pdf",
})
}
return docs, nil
}
// WriteZipToWriter writes all documents to a zip writer
func (s *ExportService) WriteZipToWriter(app *FundingApplication, w io.Writer) error {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
docs, err := s.GenerateAllDocuments(app)
if err != nil {
return err
}
for _, doc := range docs {
f, err := zipWriter.Create(doc.Name)
if err != nil {
continue
}
f.Write(doc.Content)
}
return nil
}