/** * Scope-to-Facts Mapper * * Konvertiert CompanyProfile + ScopeProfilingAnswer[] + ScopeDecision * in das Go AI SDK ScopeDecision-Format fuer POST /assess-from-scope. */ import type { CompanyProfile } from './types' import type { ScopeProfilingAnswer, ScopeDecision } from './compliance-scope-types' /** * Payload-Format fuer den Go AI SDK /assess-from-scope Endpoint. * Muss mit ucca.ScopeDecision in scope_facts_mapper.go uebereinstimmen. */ export interface ScopeDecisionPayload { employee_count: number annual_revenue: number country: string industry: string legal_form: string processes_personal_data: boolean is_controller: boolean is_processor: boolean data_art9: boolean data_minors: boolean large_scale: boolean systematic_monitoring: boolean cross_border_transfer: boolean uses_processors: boolean automated_decisions: boolean processes_employee_data: boolean processes_health_data: boolean processes_financial_data: boolean uses_cookies: boolean uses_tracking: boolean uses_video_surveillance: boolean operates_platform: boolean platform_user_count: number proc_ai_usage: boolean is_ai_provider: boolean is_ai_deployer: boolean high_risk_ai: boolean limited_risk_ai: boolean sector: string special_services: string[] is_kritis: boolean is_financial_institution: boolean determined_level: string triggered_rules: string[] required_documents: string[] cert_target: string } /** * Konvertiert CompanyProfile + Scope-Daten in das Go SDK Payload-Format. */ export function buildAssessmentPayload( profile: CompanyProfile, scopeAnswers: ScopeProfilingAnswer[], decision: ScopeDecision | null ): ScopeDecisionPayload { const getBool = (questionId: string): boolean => getAnswerBool(scopeAnswers, questionId) const getMulti = (questionId: string): string[] => getAnswerMulti(scopeAnswers, questionId) const aiCategories = getMulti('ai_categories') const isAIProvider = aiCategories.includes('ai_provider') || aiCategories.includes('ai_developer') const isAIDeployer = aiCategories.includes('ai_deployer') || aiCategories.includes('ai_operator') const aiRisk = getAnswerString(scopeAnswers, 'ai_risk_assessment') const industry = Array.isArray(profile.industry) ? profile.industry.join(', ') : (profile.industry || '') const isFinancial = industry.toLowerCase().includes('finanz') || industry.toLowerCase().includes('bank') || industry.toLowerCase().includes('versicherung') || industry.toLowerCase().includes('financial') return { employee_count: parseEmployeeRange(profile.employeeCount), annual_revenue: parseRevenueRange(profile.annualRevenue), country: profile.headquartersCountry || 'DE', industry, legal_form: profile.legalForm || '', processes_personal_data: true, // Jedes Unternehmen im Tool verarbeitet personenbezogene Daten is_controller: profile.isDataController ?? true, is_processor: profile.isDataProcessor ?? false, data_art9: getBool('data_art9'), data_minors: getBool('data_minors'), large_scale: parseEmployeeRange(profile.employeeCount) >= 250 || getBool('data_volume'), systematic_monitoring: getBool('proc_employee_monitoring') || getBool('proc_video_surveillance'), cross_border_transfer: getBool('tech_third_country'), uses_processors: getBool('tech_subprocessors'), automated_decisions: getBool('proc_adm_scoring'), processes_employee_data: getBool('data_hr'), processes_health_data: getBool('data_art9'), processes_financial_data: getBool('data_financial'), uses_cookies: getBool('prod_cookies_consent'), uses_tracking: getBool('proc_tracking'), uses_video_surveillance: getBool('proc_video_surveillance'), operates_platform: (profile.offerings || []).some(o => o === 'software_saas' || o === 'app_web' || o === 'app_mobile' ), platform_user_count: parseCustomerCount(scopeAnswers), proc_ai_usage: getBool('ai_uses_ai'), is_ai_provider: isAIProvider, is_ai_deployer: isAIDeployer, high_risk_ai: aiRisk === 'high' || aiRisk === 'unacceptable', limited_risk_ai: aiRisk === 'limited', sector: mapIndustryToSector(industry), special_services: [], is_kritis: false, // Kann spaeter aus Branche abgeleitet werden is_financial_institution: isFinancial, determined_level: decision?.determinedLevel || 'L2', triggered_rules: decision?.triggeredHardTriggers?.map(t => t.rule.id) || [], required_documents: decision?.requiredDocuments?.map(d => d.documentType) || [], cert_target: getAnswerString(scopeAnswers, 'org_cert_target'), } } // ============================================================================= // Hilfsfunktionen // ============================================================================= /** Liest eine boolean-Antwort aus den Scope-Antworten */ function getAnswerBool(answers: ScopeProfilingAnswer[], questionId: string): boolean { const answer = answers.find(a => a.questionId === questionId) if (!answer) return false if (typeof answer.value === 'boolean') return answer.value if (typeof answer.value === 'string') return answer.value === 'true' || answer.value === 'yes' || answer.value === 'ja' if (Array.isArray(answer.value)) return answer.value.length > 0 return false } /** Liest eine Multi-Select-Antwort aus den Scope-Antworten */ function getAnswerMulti(answers: ScopeProfilingAnswer[], questionId: string): string[] { const answer = answers.find(a => a.questionId === questionId) if (!answer) return [] if (Array.isArray(answer.value)) return answer.value as string[] if (typeof answer.value === 'string') return [answer.value] return [] } /** Liest einen String-Wert aus den Scope-Antworten */ function getAnswerString(answers: ScopeProfilingAnswer[], questionId: string): string { const answer = answers.find(a => a.questionId === questionId) if (!answer) return '' if (typeof answer.value === 'string') return answer.value if (Array.isArray(answer.value)) return answer.value[0] || '' return String(answer.value) } /** * Konvertiert Mitarbeiter-Range-String in einen numerischen Wert (Mittelwert). * "1-9" → 5, "10-49" → 30, "50-249" → 150, "250-999" → 625, "1000+" → 1500 */ export function parseEmployeeRange(range: string | undefined | null): number { if (!range) return 10 const r = range.trim() if (r.includes('1000') || r.includes('+')) return 1500 if (r.includes('250')) return 625 if (r.includes('50')) return 150 if (r.includes('10')) return 30 return 5 } /** * Konvertiert Umsatz-Range-String in einen numerischen Wert. * "< 2 Mio" → 1000000, "2-10 Mio" → 6000000, "10-50 Mio" → 30000000, "> 50 Mio" → 75000000 */ export function parseRevenueRange(range: string | undefined | null): number { if (!range) return 1000000 const r = range.trim().toLowerCase() if (r.includes('> 50') || r.includes('>50')) return 75000000 if (r.includes('10-50') || r.includes('10 -')) return 30000000 if (r.includes('2-10') || r.includes('2 -')) return 6000000 return 1000000 } /** Liest die Kundenzahl aus den Scope-Antworten */ function parseCustomerCount(answers: ScopeProfilingAnswer[]): number { const answer = answers.find(a => a.questionId === 'org_customer_count') if (!answer) return 0 if (typeof answer.value === 'number') return answer.value const str = String(answer.value) const match = str.match(/\d+/) return match ? parseInt(match[0], 10) : 0 } /** * Mappt deutsche Branchenbezeichnungen auf Go SDK Sektor-IDs. * Diese Sektoren werden fuer NIS2 Annex I/II Pruefung verwendet. */ function mapIndustryToSector(industry: string): string { const lower = industry.toLowerCase() // NIS2 Annex I — "Essential" Sektoren if (lower.includes('energie') || lower.includes('energy')) return 'energy' if (lower.includes('transport') || lower.includes('logistik')) return 'transport' if (lower.includes('bank') || lower.includes('finanz') || lower.includes('financial')) return 'banking' if (lower.includes('versicherung') || lower.includes('insurance')) return 'financial_market' if (lower.includes('gesundheit') || lower.includes('health') || lower.includes('pharma')) return 'health' if (lower.includes('wasser') || lower.includes('water')) return 'drinking_water' if (lower.includes('digital') || lower.includes('cloud') || lower.includes('hosting') || lower.includes('rechenzent')) return 'digital_infrastructure' if (lower.includes('telekommunikation') || lower.includes('telecom')) return 'digital_infrastructure' // NIS2 Annex II — "Important" Sektoren if (lower.includes('it') || lower.includes('software') || lower.includes('technologie')) return 'ict_service_management' if (lower.includes('lebensmittel') || lower.includes('food')) return 'food' if (lower.includes('chemie') || lower.includes('chemical')) return 'chemicals' if (lower.includes('forschung') || lower.includes('research')) return 'research' if (lower.includes('post') || lower.includes('kurier')) return 'postal' if (lower.includes('abfall') || lower.includes('waste')) return 'waste_management' if (lower.includes('fertigung') || lower.includes('manufactur') || lower.includes('produktion')) return 'manufacturing' // Fallback if (lower.includes('handel') || lower.includes('retail') || lower.includes('e-commerce')) return 'digital_providers' if (lower.includes('beratung') || lower.includes('consulting')) return 'ict_service_management' if (lower.includes('bildung') || lower.includes('education')) return 'research' if (lower.includes('medien') || lower.includes('media')) return 'digital_providers' return 'other' }