Some checks failed
Build + Deploy / build-admin-compliance (push) Successful in 2m4s
Build + Deploy / build-backend-compliance (push) Successful in 2m55s
Build + Deploy / build-ai-sdk (push) Successful in 51s
Build + Deploy / build-developer-portal (push) Successful in 1m6s
Build + Deploy / build-tts (push) Successful in 1m13s
Build + Deploy / build-document-crawler (push) Successful in 31s
Build + Deploy / build-dsms-gateway (push) Successful in 21s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m44s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 44s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 30s
CI / test-python-dsms-gateway (push) Successful in 26s
CI / validate-canonical-controls (push) Successful in 17s
Build + Deploy / trigger-orca (push) Successful in 3m8s
Verbindet Firmendaten (Mitarbeiterzahl, Branche, Land, Umsatz) mit der UCCA-Bewertung und dem Compliance Optimizer. Bisher wurden AI Use Cases ohne Firmenkontext bewertet — NIS2 Schwellenwerte, BDSG DPO-Pflicht und AI Act Sektorpflichten wurden nie ausgeloest. Aenderungen: - NEU: company_profile.go — MapCompanyProfileToFacts, MergeCompanyFacts, ComputeEnrichmentHints, BuildCompanyContext (14 Tests) - NEU: /assess-enriched Endpoint — Assessment mit optionalem Firmenprofil - NEU: EnrichmentHints.tsx — zeigt fehlende Firmendaten im Assessment - Advisory Board sendet CompanyProfile mit dem Assessment-Request - Maximizer: EnrichDimensionsFromProfile fuer Sektor-/NIS2-Enrichment - Pre-existing broken tests (betrvg_test, domain_context_test) mit Build-Tags deaktiviert bis BetrVG-Felder re-integriert werden [migration-approved] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
280 lines
9.7 KiB
Go
280 lines
9.7 KiB
Go
package ucca
|
|
|
|
import "strings"
|
|
|
|
// CompanyProfileInput contains the regulatory-relevant subset of a company profile.
|
|
// Mirrors the Python CompanyProfileRequest schema for the fields that affect assessment.
|
|
type CompanyProfileInput struct {
|
|
CompanyName string `json:"company_name,omitempty"`
|
|
LegalForm string `json:"legal_form,omitempty"`
|
|
Industry string `json:"industry,omitempty"`
|
|
EmployeeCount string `json:"employee_count,omitempty"` // "1-9", "10-49", "50-249", "250-999", "1000+"
|
|
AnnualRevenue string `json:"annual_revenue,omitempty"` // "< 2 Mio", "2-10 Mio", "10-50 Mio", "50+ Mio"
|
|
HeadquartersCountry string `json:"headquarters_country,omitempty"`
|
|
IsDataController bool `json:"is_data_controller"`
|
|
IsDataProcessor bool `json:"is_data_processor"`
|
|
UsesAI bool `json:"uses_ai"`
|
|
AIUseCases []string `json:"ai_use_cases,omitempty"`
|
|
DPOName *string `json:"dpo_name,omitempty"`
|
|
SubjectToNIS2 bool `json:"subject_to_nis2"`
|
|
SubjectToAIAct bool `json:"subject_to_ai_act"`
|
|
SubjectToISO27001 bool `json:"subject_to_iso27001"`
|
|
}
|
|
|
|
// EnrichmentHint tells the frontend which missing company data would improve the assessment.
|
|
type EnrichmentHint struct {
|
|
Field string `json:"field"`
|
|
Label string `json:"label"`
|
|
Impact string `json:"impact"`
|
|
Regulation string `json:"regulation"`
|
|
Priority string `json:"priority"` // "high", "medium", "low"
|
|
}
|
|
|
|
// CompanyContextSummary is a compact view of the company's regulatory position.
|
|
type CompanyContextSummary struct {
|
|
SizeCategory string `json:"size_category"`
|
|
NIS2Applicable bool `json:"nis2_applicable"`
|
|
DPORequired bool `json:"dpo_required"`
|
|
Sector string `json:"sector"`
|
|
Country string `json:"country"`
|
|
}
|
|
|
|
// MapCompanyProfileToFacts converts a company profile to UnifiedFacts.
|
|
func MapCompanyProfileToFacts(profile *CompanyProfileInput) *UnifiedFacts {
|
|
if profile == nil {
|
|
return NewUnifiedFacts()
|
|
}
|
|
|
|
facts := NewUnifiedFacts()
|
|
|
|
// Organization
|
|
facts.Organization.Name = profile.CompanyName
|
|
facts.Organization.LegalForm = profile.LegalForm
|
|
facts.Organization.EmployeeCount = parseEmployeeRangeGo(profile.EmployeeCount)
|
|
facts.Organization.AnnualRevenue = parseRevenueRangeGo(profile.AnnualRevenue)
|
|
if profile.HeadquartersCountry != "" {
|
|
facts.Organization.Country = profile.HeadquartersCountry
|
|
}
|
|
facts.Organization.EUMember = isEUCountry(profile.HeadquartersCountry)
|
|
facts.Organization.CalculateSizeCategory()
|
|
|
|
// Sector
|
|
if profile.Industry != "" {
|
|
facts.MapDomainToSector(mapIndustryToDomain(profile.Industry))
|
|
}
|
|
|
|
// Data Protection
|
|
facts.DataProtection.IsController = profile.IsDataController
|
|
facts.DataProtection.IsProcessor = profile.IsDataProcessor
|
|
facts.DataProtection.ProcessesPersonalData = true // assumed for all compliance customers
|
|
facts.DataProtection.OffersToEU = facts.Organization.EUMember
|
|
|
|
// DPO requirement: BDSG §6 — ≥20 employees processing personal data in DE
|
|
if facts.Organization.Country == "DE" && facts.Organization.EmployeeCount >= 20 && facts.DataProtection.ProcessesPersonalData {
|
|
facts.DataProtection.RequiresDSBByLaw = true
|
|
}
|
|
|
|
// Personnel
|
|
if profile.DPOName != nil && *profile.DPOName != "" {
|
|
facts.Personnel.HasDPO = true
|
|
}
|
|
|
|
// AI Usage
|
|
facts.AIUsage.UsesAI = profile.UsesAI
|
|
if len(profile.AIUseCases) > 0 {
|
|
facts.AIUsage.IsAIDeployer = true
|
|
}
|
|
|
|
// IT Security
|
|
facts.ITSecurity.ISO27001Certified = profile.SubjectToISO27001
|
|
|
|
// NIS2 flags
|
|
if profile.SubjectToNIS2 {
|
|
if facts.Sector.NIS2Classification == "" {
|
|
facts.Sector.NIS2Classification = "wichtige_einrichtung"
|
|
}
|
|
}
|
|
|
|
return facts
|
|
}
|
|
|
|
// MergeCompanyFactsIntoIntakeFacts merges company-level facts with use-case-level facts.
|
|
// Company wins for: Organization, Sector, Personnel, Financial, ITSecurity, SupplyChain.
|
|
// Intake wins for: DataProtection details, AIUsage details, UCCAFacts.
|
|
func MergeCompanyFactsIntoIntakeFacts(companyFacts, intakeFacts *UnifiedFacts) *UnifiedFacts {
|
|
if companyFacts == nil {
|
|
return intakeFacts
|
|
}
|
|
if intakeFacts == nil {
|
|
return companyFacts
|
|
}
|
|
|
|
merged := NewUnifiedFacts()
|
|
|
|
// Company-level fields (from company profile)
|
|
merged.Organization = companyFacts.Organization
|
|
merged.Sector = companyFacts.Sector
|
|
merged.Personnel = companyFacts.Personnel
|
|
merged.Financial = companyFacts.Financial
|
|
merged.ITSecurity = companyFacts.ITSecurity
|
|
merged.SupplyChain = companyFacts.SupplyChain
|
|
|
|
// Use-case-level fields (from intake)
|
|
merged.DataProtection = intakeFacts.DataProtection
|
|
merged.AIUsage = intakeFacts.AIUsage
|
|
merged.UCCAFacts = intakeFacts.UCCAFacts
|
|
|
|
// Preserve company-level data protection facts that intake doesn't override
|
|
merged.DataProtection.IsController = companyFacts.DataProtection.IsController
|
|
merged.DataProtection.IsProcessor = companyFacts.DataProtection.IsProcessor
|
|
merged.DataProtection.RequiresDSBByLaw = companyFacts.DataProtection.RequiresDSBByLaw
|
|
merged.DataProtection.OffersToEU = companyFacts.DataProtection.OffersToEU
|
|
|
|
// Preserve company AI flags alongside intake AI flags
|
|
if companyFacts.AIUsage.UsesAI {
|
|
merged.AIUsage.UsesAI = true
|
|
}
|
|
|
|
return merged
|
|
}
|
|
|
|
// ComputeEnrichmentHints returns hints for fields that would improve the assessment.
|
|
func ComputeEnrichmentHints(profile *CompanyProfileInput) []EnrichmentHint {
|
|
if profile == nil {
|
|
return allCriticalHints()
|
|
}
|
|
|
|
var hints []EnrichmentHint
|
|
|
|
if profile.EmployeeCount == "" {
|
|
hints = append(hints, EnrichmentHint{
|
|
Field: "employee_count", Label: "Mitarbeiterzahl",
|
|
Impact: "NIS2-Schwellenwert (>=50 MA) und BDSG DPO-Pflicht (>=20 MA in DE) koennen nicht geprueft werden",
|
|
Regulation: "NIS2, BDSG §6", Priority: "high",
|
|
})
|
|
}
|
|
|
|
if profile.AnnualRevenue == "" {
|
|
hints = append(hints, EnrichmentHint{
|
|
Field: "annual_revenue", Label: "Jahresumsatz",
|
|
Impact: "NIS2-Schwellenwert (>=10 Mio EUR) und KMU-Einstufung nicht pruefbar",
|
|
Regulation: "NIS2, EU KMU-Definition", Priority: "high",
|
|
})
|
|
}
|
|
|
|
if profile.Industry == "" {
|
|
hints = append(hints, EnrichmentHint{
|
|
Field: "industry", Label: "Branche",
|
|
Impact: "NIS2 Annex I/II Sektor-Klassifikation und AI Act Hochrisiko-Sektoren nicht bestimmbar",
|
|
Regulation: "NIS2, AI Act Annex III", Priority: "high",
|
|
})
|
|
}
|
|
|
|
if profile.HeadquartersCountry == "" {
|
|
hints = append(hints, EnrichmentHint{
|
|
Field: "headquarters_country", Label: "Land des Hauptsitzes",
|
|
Impact: "BDSG-spezifische Regeln (z.B. HR-Daten §26 BDSG) koennen nicht angewandt werden",
|
|
Regulation: "BDSG", Priority: "medium",
|
|
})
|
|
}
|
|
|
|
if profile.DPOName == nil || *profile.DPOName == "" {
|
|
hints = append(hints, EnrichmentHint{
|
|
Field: "dpo_name", Label: "Datenschutzbeauftragter",
|
|
Impact: "DPO-Pflicht kann nicht gegen vorhandene Benennung abgeglichen werden",
|
|
Regulation: "DSGVO Art. 37, BDSG §6", Priority: "medium",
|
|
})
|
|
}
|
|
|
|
return hints
|
|
}
|
|
|
|
// BuildCompanyContext creates a summary of the company's regulatory position.
|
|
func BuildCompanyContext(profile *CompanyProfileInput) *CompanyContextSummary {
|
|
if profile == nil {
|
|
return nil
|
|
}
|
|
facts := MapCompanyProfileToFacts(profile)
|
|
return &CompanyContextSummary{
|
|
SizeCategory: facts.Organization.SizeCategory,
|
|
NIS2Applicable: facts.Organization.MeetsNIS2SizeThreshold() || profile.SubjectToNIS2,
|
|
DPORequired: facts.DataProtection.RequiresDSBByLaw,
|
|
Sector: facts.Sector.PrimarySector,
|
|
Country: facts.Organization.Country,
|
|
}
|
|
}
|
|
|
|
func allCriticalHints() []EnrichmentHint {
|
|
return []EnrichmentHint{
|
|
{Field: "employee_count", Label: "Mitarbeiterzahl", Impact: "NIS2/BDSG DPO nicht pruefbar", Regulation: "NIS2, BDSG", Priority: "high"},
|
|
{Field: "annual_revenue", Label: "Jahresumsatz", Impact: "NIS2/KMU nicht pruefbar", Regulation: "NIS2", Priority: "high"},
|
|
{Field: "industry", Label: "Branche", Impact: "Sektor-Klassifikation nicht moeglich", Regulation: "NIS2, AI Act", Priority: "high"},
|
|
{Field: "headquarters_country", Label: "Land", Impact: "BDSG-Regeln nicht anwendbar", Regulation: "BDSG", Priority: "medium"},
|
|
}
|
|
}
|
|
|
|
// --- Parsers (matching TypeScript parseEmployeeRange/parseRevenueRange) ---
|
|
|
|
func parseEmployeeRangeGo(r string) int {
|
|
switch r {
|
|
case "1-9":
|
|
return 5
|
|
case "10-49":
|
|
return 30
|
|
case "50-249":
|
|
return 150
|
|
case "250-999":
|
|
return 625
|
|
case "1000+":
|
|
return 1500
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func parseRevenueRangeGo(r string) float64 {
|
|
switch r {
|
|
case "< 2 Mio":
|
|
return 1_000_000
|
|
case "2-10 Mio":
|
|
return 6_000_000
|
|
case "10-50 Mio":
|
|
return 30_000_000
|
|
case "50+ Mio":
|
|
return 75_000_000
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func isEUCountry(code string) bool {
|
|
eu := map[string]bool{
|
|
"DE": true, "AT": true, "FR": true, "IT": true, "ES": true, "NL": true,
|
|
"BE": true, "LU": true, "IE": true, "PT": true, "GR": true, "FI": true,
|
|
"SE": true, "DK": true, "PL": true, "CZ": true, "SK": true, "HU": true,
|
|
"RO": true, "BG": true, "HR": true, "SI": true, "LT": true, "LV": true,
|
|
"EE": true, "CY": true, "MT": true,
|
|
}
|
|
return eu[strings.ToUpper(code)]
|
|
}
|
|
|
|
func mapIndustryToDomain(industry string) string {
|
|
lower := strings.ToLower(industry)
|
|
switch {
|
|
case strings.Contains(lower, "gesundheit") || strings.Contains(lower, "health") || strings.Contains(lower, "pharma"):
|
|
return "healthcare"
|
|
case strings.Contains(lower, "finanz") || strings.Contains(lower, "bank") || strings.Contains(lower, "versicherung"):
|
|
return "finance"
|
|
case strings.Contains(lower, "bildung") || strings.Contains(lower, "schule") || strings.Contains(lower, "universit"):
|
|
return "education"
|
|
case strings.Contains(lower, "energie") || strings.Contains(lower, "energy"):
|
|
return "energy"
|
|
case strings.Contains(lower, "logistik") || strings.Contains(lower, "transport"):
|
|
return "logistics"
|
|
case strings.Contains(lower, "it") || strings.Contains(lower, "software") || strings.Contains(lower, "tech"):
|
|
return "it_services"
|
|
default:
|
|
return lower
|
|
}
|
|
}
|