package handlers import ( "fmt" "net/http" "os" "time" "github.com/breakpilot/ai-compliance-sdk/internal/funding" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/gin-gonic/gin" "github.com/google/uuid" "gopkg.in/yaml.v3" ) // FundingHandlers handles funding application API endpoints type FundingHandlers struct { store funding.Store providerRegistry *llm.ProviderRegistry wizardSchema *WizardSchema bundeslandProfiles map[string]*BundeslandProfile } // WizardSchema represents the loaded wizard schema type WizardSchema struct { Metadata struct { Version string `yaml:"version"` Name string `yaml:"name"` Description string `yaml:"description"` TotalSteps int `yaml:"total_steps"` } `yaml:"metadata"` Steps []WizardStep `yaml:"steps"` FundingAssistant struct { Enabled bool `yaml:"enabled"` Model string `yaml:"model"` SystemPrompt string `yaml:"system_prompt"` StepContexts map[int]string `yaml:"step_contexts"` QuickPrompts []QuickPrompt `yaml:"quick_prompts"` } `yaml:"funding_assistant"` Presets map[string]Preset `yaml:"presets"` } // WizardStep represents a step in the wizard type WizardStep struct { Number int `yaml:"number" json:"number"` ID string `yaml:"id" json:"id"` Title string `yaml:"title" json:"title"` Subtitle string `yaml:"subtitle" json:"subtitle"` Description string `yaml:"description" json:"description"` Icon string `yaml:"icon" json:"icon"` IsRequired bool `yaml:"is_required" json:"is_required"` Fields []WizardField `yaml:"fields" json:"fields"` AssistantContext string `yaml:"assistant_context" json:"assistant_context"` } // WizardField represents a field in the wizard type WizardField struct { ID string `yaml:"id" json:"id"` Type string `yaml:"type" json:"type"` Label string `yaml:"label" json:"label"` Placeholder string `yaml:"placeholder,omitempty" json:"placeholder,omitempty"` Required bool `yaml:"required,omitempty" json:"required,omitempty"` Options []FieldOption `yaml:"options,omitempty" json:"options,omitempty"` HelpText string `yaml:"help_text,omitempty" json:"help_text,omitempty"` MaxLength int `yaml:"max_length,omitempty" json:"max_length,omitempty"` Min *int `yaml:"min,omitempty" json:"min,omitempty"` Max *int `yaml:"max,omitempty" json:"max,omitempty"` Default interface{} `yaml:"default,omitempty" json:"default,omitempty"` Conditional string `yaml:"conditional,omitempty" json:"conditional,omitempty"` } // FieldOption represents an option for select fields type FieldOption struct { Value string `yaml:"value" json:"value"` Label string `yaml:"label" json:"label"` Description string `yaml:"description,omitempty" json:"description,omitempty"` } // QuickPrompt represents a quick prompt for the assistant type QuickPrompt struct { Label string `yaml:"label" json:"label"` Prompt string `yaml:"prompt" json:"prompt"` } // Preset represents a BreakPilot preset type Preset struct { ID string `yaml:"id" json:"id"` Name string `yaml:"name" json:"name"` Description string `yaml:"description" json:"description"` BudgetItems []funding.BudgetItem `yaml:"budget_items" json:"budget_items"` AutoFill map[string]interface{} `yaml:"auto_fill" json:"auto_fill"` } // BundeslandProfile represents a federal state profile type BundeslandProfile struct { Name string `yaml:"name" json:"name"` Short string `yaml:"short" json:"short"` FundingPrograms []string `yaml:"funding_programs" json:"funding_programs"` DefaultFundingRate float64 `yaml:"default_funding_rate" json:"default_funding_rate"` RequiresMEP bool `yaml:"requires_mep" json:"requires_mep"` ContactAuthority ContactAuthority `yaml:"contact_authority" json:"contact_authority"` SpecialRequirements []string `yaml:"special_requirements" json:"special_requirements"` } // ContactAuthority represents a contact authority type ContactAuthority struct { Name string `yaml:"name" json:"name"` Department string `yaml:"department,omitempty" json:"department,omitempty"` Website string `yaml:"website" json:"website"` Email string `yaml:"email,omitempty" json:"email,omitempty"` } // NewFundingHandlers creates new funding handlers func NewFundingHandlers(store funding.Store, providerRegistry *llm.ProviderRegistry) *FundingHandlers { h := &FundingHandlers{ store: store, providerRegistry: providerRegistry, } // Load wizard schema if err := h.loadWizardSchema(); err != nil { fmt.Printf("Warning: Could not load wizard schema: %v\n", err) } // Load bundesland profiles if err := h.loadBundeslandProfiles(); err != nil { fmt.Printf("Warning: Could not load bundesland profiles: %v\n", err) } return h } func (h *FundingHandlers) loadWizardSchema() error { data, err := os.ReadFile("policies/funding/foerderantrag_wizard_v1.yaml") if err != nil { return err } h.wizardSchema = &WizardSchema{} return yaml.Unmarshal(data, h.wizardSchema) } func (h *FundingHandlers) loadBundeslandProfiles() error { data, err := os.ReadFile("policies/funding/bundesland_profiles.yaml") if err != nil { return err } var profiles struct { Bundeslaender map[string]*BundeslandProfile `yaml:"bundeslaender"` } if err := yaml.Unmarshal(data, &profiles); err != nil { return err } h.bundeslandProfiles = profiles.Bundeslaender return nil } // ============================================================================ // Application CRUD // ============================================================================ // CreateApplication creates a new funding application // POST /sdk/v1/funding/applications func (h *FundingHandlers) CreateApplication(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } var req funding.CreateApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } app := &funding.FundingApplication{ TenantID: tenantID, Title: req.Title, FundingProgram: req.FundingProgram, Status: funding.ApplicationStatusDraft, CurrentStep: 1, TotalSteps: 8, WizardData: make(map[string]interface{}), CreatedBy: userID, UpdatedBy: userID, } // Initialize school profile with federal state app.SchoolProfile = &funding.SchoolProfile{ FederalState: req.FederalState, } // Apply preset if specified if req.PresetID != "" && h.wizardSchema != nil { if preset, ok := h.wizardSchema.Presets[req.PresetID]; ok { app.Budget = &funding.Budget{ BudgetItems: preset.BudgetItems, } app.WizardData["preset_id"] = req.PresetID app.WizardData["preset_applied"] = true for k, v := range preset.AutoFill { app.WizardData[k] = v } } } if err := h.store.CreateApplication(c.Request.Context(), app); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add history entry h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{ ApplicationID: app.ID, Action: "created", PerformedBy: userID, Notes: "Antrag erstellt", }) c.JSON(http.StatusCreated, app) } // GetApplication retrieves a funding application // GET /sdk/v1/funding/applications/:id func (h *FundingHandlers) GetApplication(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } app, err := h.store.GetApplication(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, app) } // ListApplications returns a list of funding applications // GET /sdk/v1/funding/applications func (h *FundingHandlers) ListApplications(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } filter := funding.ApplicationFilter{ Page: 1, PageSize: 20, } // Parse query parameters if status := c.Query("status"); status != "" { s := funding.ApplicationStatus(status) filter.Status = &s } if program := c.Query("program"); program != "" { p := funding.FundingProgram(program) filter.FundingProgram = &p } result, err := h.store.ListApplications(c.Request.Context(), tenantID, filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // UpdateApplication updates a funding application // PUT /sdk/v1/funding/applications/:id func (h *FundingHandlers) UpdateApplication(c *gin.Context) { userID := rbac.GetUserID(c) idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } app, err := h.store.GetApplication(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } var req funding.UpdateApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Title != nil { app.Title = *req.Title } if req.WizardData != nil { for k, v := range req.WizardData { app.WizardData[k] = v } } if req.CurrentStep != nil { app.CurrentStep = *req.CurrentStep } app.UpdatedBy = userID if err := h.store.UpdateApplication(c.Request.Context(), app); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, app) } // DeleteApplication deletes a funding application // DELETE /sdk/v1/funding/applications/:id func (h *FundingHandlers) DeleteApplication(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } if err := h.store.DeleteApplication(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "application archived"}) } // ============================================================================ // Wizard Endpoints // ============================================================================ // GetWizardSchema returns the wizard schema // GET /sdk/v1/funding/wizard/schema func (h *FundingHandlers) GetWizardSchema(c *gin.Context) { if h.wizardSchema == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "wizard schema not loaded"}) return } c.JSON(http.StatusOK, gin.H{ "metadata": h.wizardSchema.Metadata, "steps": h.wizardSchema.Steps, "presets": h.wizardSchema.Presets, "assistant": gin.H{ "enabled": h.wizardSchema.FundingAssistant.Enabled, "quick_prompts": h.wizardSchema.FundingAssistant.QuickPrompts, }, }) } // SaveWizardStep saves wizard step data // POST /sdk/v1/funding/applications/:id/wizard func (h *FundingHandlers) SaveWizardStep(c *gin.Context) { userID := rbac.GetUserID(c) idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } var req funding.SaveWizardStepRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Save step data if err := h.store.SaveWizardStep(c.Request.Context(), id, req.Step, req.Data); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Get updated progress progress, err := h.store.GetWizardProgress(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add history entry h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{ ApplicationID: id, Action: "wizard_step_saved", PerformedBy: userID, Notes: fmt.Sprintf("Schritt %d gespeichert", req.Step), }) c.JSON(http.StatusOK, progress) } // AskAssistant handles LLM assistant queries // POST /sdk/v1/funding/wizard/ask func (h *FundingHandlers) AskAssistant(c *gin.Context) { var req funding.AssistantRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if h.wizardSchema == nil || !h.wizardSchema.FundingAssistant.Enabled { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "assistant not available"}) return } // Build system prompt with step context systemPrompt := h.wizardSchema.FundingAssistant.SystemPrompt if stepContext, ok := h.wizardSchema.FundingAssistant.StepContexts[req.CurrentStep]; ok { systemPrompt += "\n\nKontext fuer diesen Schritt:\n" + stepContext } // Build messages messages := []llm.Message{ {Role: "system", Content: systemPrompt}, } for _, msg := range req.History { messages = append(messages, llm.Message{ Role: msg.Role, Content: msg.Content, }) } messages = append(messages, llm.Message{ Role: "user", Content: req.Question, }) // Generate response using registry chatReq := &llm.ChatRequest{ Messages: messages, Temperature: 0.3, MaxTokens: 1000, } response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, funding.AssistantResponse{ Answer: response.Message.Content, }) } // ============================================================================ // Status Endpoints // ============================================================================ // SubmitApplication submits an application for review // POST /sdk/v1/funding/applications/:id/submit func (h *FundingHandlers) SubmitApplication(c *gin.Context) { userID := rbac.GetUserID(c) idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } app, err := h.store.GetApplication(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } // Validate that all required steps are completed progress, _ := h.store.GetWizardProgress(c.Request.Context(), id) if progress == nil || len(progress.CompletedSteps) < app.TotalSteps { c.JSON(http.StatusBadRequest, gin.H{"error": "not all required steps completed"}) return } // Update status app.Status = funding.ApplicationStatusSubmitted now := time.Now() app.SubmittedAt = &now app.UpdatedBy = userID if err := h.store.UpdateApplication(c.Request.Context(), app); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add history entry h.store.AddHistoryEntry(c.Request.Context(), &funding.ApplicationHistoryEntry{ ApplicationID: id, Action: "submitted", PerformedBy: userID, Notes: "Antrag eingereicht", }) c.JSON(http.StatusOK, app) } // ============================================================================ // Export Endpoints // ============================================================================ // ExportApplication exports all documents as ZIP // GET /sdk/v1/funding/applications/:id/export func (h *FundingHandlers) ExportApplication(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } app, err := h.store.GetApplication(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } // Generate export (this will be implemented in export.go) // For now, return a placeholder response c.JSON(http.StatusOK, gin.H{ "message": "Export generation initiated", "application_id": app.ID, "status": "processing", }) } // PreviewApplication generates a PDF preview // GET /sdk/v1/funding/applications/:id/preview func (h *FundingHandlers) PreviewApplication(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } app, err := h.store.GetApplication(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } // Generate PDF preview (placeholder) c.JSON(http.StatusOK, gin.H{ "message": "Preview generation initiated", "application_id": app.ID, }) } // ============================================================================ // Bundesland Profile Endpoints // ============================================================================ // GetBundeslandProfiles returns all bundesland profiles // GET /sdk/v1/funding/bundeslaender func (h *FundingHandlers) GetBundeslandProfiles(c *gin.Context) { if h.bundeslandProfiles == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "bundesland profiles not loaded"}) return } c.JSON(http.StatusOK, h.bundeslandProfiles) } // GetBundeslandProfile returns a specific bundesland profile // GET /sdk/v1/funding/bundeslaender/:state func (h *FundingHandlers) GetBundeslandProfile(c *gin.Context) { state := c.Param("state") if h.bundeslandProfiles == nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "bundesland profiles not loaded"}) return } profile, ok := h.bundeslandProfiles[state] if !ok { c.JSON(http.StatusNotFound, gin.H{"error": "bundesland not found"}) return } c.JSON(http.StatusOK, profile) } // ============================================================================ // Statistics Endpoint // ============================================================================ // GetStatistics returns funding statistics // GET /sdk/v1/funding/statistics func (h *FundingHandlers) GetStatistics(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } stats, err := h.store.GetStatistics(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // ============================================================================ // History Endpoint // ============================================================================ // GetApplicationHistory returns the audit trail // GET /sdk/v1/funding/applications/:id/history func (h *FundingHandlers) GetApplicationHistory(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid application ID"}) return } history, err := h.store.GetHistory(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, history) }