Files
breakpilot-compliance/admin-compliance/lib/sdk/compliance-scope-profiling.ts
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

822 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type {
ScopeQuestionBlock,
ScopeQuestionBlockId,
ScopeProfilingQuestion,
ScopeProfilingAnswer,
ComplianceScopeState,
} from './compliance-scope-types'
import type { CompanyProfile } from './types'
/**
* Block 1: Organisation & Reife
*/
const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
id: 'organisation',
title: 'Organisation & Reife',
description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
order: 1,
questions: [
{
id: 'org_employee_count',
type: 'number',
question: 'Wie viele Mitarbeiter hat Ihre Organisation?',
helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)',
required: true,
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
mapsToCompanyProfile: 'employeeCount',
},
{
id: 'org_customer_count',
type: 'single',
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
required: true,
options: [
{ value: '<100', label: 'Weniger als 100' },
{ value: '100-1000', label: '100 bis 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000+', label: 'Mehr als 100.000' },
],
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
},
{
id: 'org_annual_revenue',
type: 'single',
question: 'Wie hoch ist Ihr jährlicher Umsatz?',
helpText: 'Wählen Sie die zutreffende Umsatzklasse',
required: true,
options: [
{ value: '<2Mio', label: 'Unter 2 Mio. EUR' },
{ value: '2-10Mio', label: '2 bis 10 Mio. EUR' },
{ value: '10-50Mio', label: '10 bis 50 Mio. EUR' },
{ value: '>50Mio', label: 'Über 50 Mio. EUR' },
],
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
mapsToCompanyProfile: 'annualRevenue',
},
{
id: 'org_cert_target',
type: 'multi',
question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
required: false,
options: [
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
{ value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
{ value: 'TISAX', label: 'TISAX (Automotive)' },
{ value: 'SOC2', label: 'SOC 2 (US-Standard)' },
{ value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
{ value: 'Keine', label: 'Keine Zertifizierung geplant' },
],
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
},
{
id: 'org_industry',
type: 'single',
question: 'In welcher Branche sind Sie tätig?',
helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
required: true,
options: [
{ value: 'it_software', label: 'IT & Software' },
{ value: 'healthcare', label: 'Gesundheitswesen' },
{ value: 'education', label: 'Bildung & Forschung' },
{ value: 'finance', label: 'Finanzdienstleistungen' },
{ value: 'retail', label: 'Einzelhandel & E-Commerce' },
{ value: 'manufacturing', label: 'Produktion & Fertigung' },
{ value: 'consulting', label: 'Beratung & Dienstleistungen' },
{ value: 'public', label: 'Öffentliche Verwaltung' },
{ value: 'other', label: 'Sonstige' },
],
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
mapsToCompanyProfile: 'industry',
mapsToVVTQuestion: 'org_industry',
mapsToLFQuestion: 'org-branche',
},
{
id: 'org_business_model',
type: 'single',
question: 'Was ist Ihr primäres Geschäftsmodell?',
helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen',
required: true,
options: [
{ value: 'b2b', label: 'B2B (Business-to-Business)' },
{ value: 'b2c', label: 'B2C (Business-to-Consumer)' },
{ value: 'both', label: 'B2B und B2C gemischt' },
{ value: 'b2g', label: 'B2G (Business-to-Government)' },
],
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
mapsToCompanyProfile: 'businessModel',
mapsToVVTQuestion: 'org_b2b_b2c',
mapsToLFQuestion: 'org-geschaeftsmodell',
},
{
id: 'org_has_dsb',
type: 'boolean',
question: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht',
required: true,
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
},
],
}
/**
* Block 2: Daten & Betroffene
*/
const BLOCK_2_DATA: ScopeQuestionBlock = {
id: 'data',
title: 'Daten & Betroffene',
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
order: 2,
questions: [
{
id: 'data_minors',
type: 'boolean',
question: 'Verarbeiten Sie Daten von Minderjährigen?',
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
required: true,
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'data_minors',
},
{
id: 'data_art9',
type: 'multi',
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
required: true,
options: [
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
{ value: 'genetik', label: 'Genetische Daten' },
{ value: 'politisch', label: 'Politische Meinungen' },
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
],
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
mapsToVVTQuestion: 'data_health',
},
{
id: 'data_hr',
type: 'boolean',
question: 'Verarbeiten Sie Personaldaten (HR)?',
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
required: true,
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
mapsToVVTQuestion: 'dept_hr',
mapsToLFQuestion: 'data-hr',
},
{
id: 'data_communication',
type: 'boolean',
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
required: true,
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
},
{
id: 'data_financial',
type: 'boolean',
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
mapsToVVTQuestion: 'dept_finance',
mapsToLFQuestion: 'data-buchhaltung',
},
{
id: 'data_volume',
type: 'single',
question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
required: true,
options: [
{ value: '<1000', label: 'Unter 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000-1000000', label: '100.000 bis 1 Mio.' },
{ value: '>1000000', label: 'Über 1 Mio.' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 3: Verarbeitung & Zweck
*/
const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
id: 'processing',
title: 'Verarbeitung & Zweck',
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
order: 3,
questions: [
{
id: 'proc_tracking',
type: 'boolean',
question: 'Setzen Sie Tracking oder Profiling ein?',
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'proc_adm_scoring',
type: 'boolean',
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
},
{
id: 'proc_ai_usage',
type: 'multi',
question: 'Setzen Sie KI-Systeme ein?',
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
required: true,
options: [
{ value: 'keine', label: 'Keine KI im Einsatz' },
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
],
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
},
{
id: 'proc_data_combination',
type: 'boolean',
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
helpText: 'Data Matching, Anreicherung aus externen Quellen',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
},
{
id: 'proc_employee_monitoring',
type: 'boolean',
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
},
{
id: 'proc_video_surveillance',
type: 'boolean',
question: 'Setzen Sie Videoüberwachung ein?',
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
required: true,
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'special_video_surveillance',
mapsToLFQuestion: 'data-video',
},
],
}
/**
* Block 4: Technik/Hosting/Transfers
*/
const BLOCK_4_TECH: ScopeQuestionBlock = {
id: 'tech',
title: 'Technik, Hosting & Transfers',
description: 'Technische Infrastruktur und Datenübermittlung',
order: 4,
questions: [
{
id: 'tech_hosting_location',
type: 'single',
question: 'Wo werden Ihre Daten primär gehostet?',
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
required: true,
options: [
{ value: 'de', label: 'Deutschland' },
{ value: 'eu', label: 'EU (ohne Deutschland)' },
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
},
{
id: 'tech_subprocessors',
type: 'boolean',
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. erfordert AVV nach Art. 28 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
},
{
id: 'tech_third_country',
type: 'boolean',
question: 'Übermitteln Sie Daten in Drittländer?',
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
mapsToVVTQuestion: 'transfer_cloud_us',
},
{
id: 'tech_encryption_rest',
type: 'boolean',
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
required: true,
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
},
{
id: 'tech_encryption_transit',
type: 'boolean',
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
helpText: 'TLS/SSL für alle Verbindungen',
required: true,
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
},
{
id: 'tech_cloud_providers',
type: 'multi',
question: 'Welche Cloud-Anbieter nutzen Sie?',
helpText: 'Mehrfachauswahl möglich',
required: false,
options: [
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
{ value: 'azure', label: 'Microsoft Azure' },
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
{ value: 'hetzner', label: 'Hetzner' },
{ value: 'ionos', label: 'IONOS' },
{ value: 'ovh', label: 'OVH' },
{ value: 'andere', label: 'Andere Anbieter' },
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 5: Rechte & Prozesse
*/
const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
id: 'processes',
title: 'Rechte & Prozesse',
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
order: 5,
questions: [
{
id: 'proc_dsar_process',
type: 'boolean',
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. Art. 15-22 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
},
{
id: 'proc_deletion_concept',
type: 'boolean',
question: 'Haben Sie ein Löschkonzept?',
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
},
{
id: 'proc_incident_response',
type: 'boolean',
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
},
{
id: 'proc_regular_audits',
type: 'boolean',
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
},
{
id: 'proc_training',
type: 'boolean',
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
required: true,
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
},
],
}
/**
* Block 6: Produktkontext
*/
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
id: 'product',
title: 'Produktkontext',
description: 'Spezifische Merkmale Ihrer Produkte und Services',
order: 6,
questions: [
{
id: 'prod_type',
type: 'multi',
question: 'Welche Art von Produkten/Services bieten Sie an?',
helpText: 'Mehrfachauswahl möglich',
required: true,
options: [
{ value: 'webapp', label: 'Web-Anwendung' },
{ value: 'mobile', label: 'Mobile App (iOS/Android)' },
{ value: 'saas', label: 'SaaS-Plattform' },
{ value: 'onpremise', label: 'On-Premise Software' },
{ value: 'api', label: 'API/Schnittstellen' },
{ value: 'iot', label: 'IoT/Hardware' },
{ value: 'beratung', label: 'Beratungsleistungen' },
{ value: 'handel', label: 'Handel/Vertrieb' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
},
{
id: 'prod_cookies_consent',
type: 'boolean',
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
},
{
id: 'prod_webshop',
type: 'boolean',
question: 'Betreiben Sie einen Online-Shop?',
helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'prod_api_external',
type: 'boolean',
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
},
{
id: 'prod_data_broker',
type: 'boolean',
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
required: true,
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
},
],
}
/**
* All question blocks in order
*/
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
BLOCK_1_ORGANISATION,
BLOCK_2_DATA,
BLOCK_3_PROCESSING,
BLOCK_4_TECH,
BLOCK_5_PROCESSES,
BLOCK_6_PRODUCT,
]
/**
* Prefill scope answers from CompanyProfile
*/
export function prefillFromCompanyProfile(
profile: CompanyProfile
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// employeeCount
if (profile.employeeCount != null) {
answers.push({
questionId: 'org_employee_count',
value: profile.employeeCount,
})
}
// annualRevenue
if (profile.annualRevenue) {
answers.push({
questionId: 'org_annual_revenue',
value: profile.annualRevenue,
})
}
// industry
if (profile.industry) {
answers.push({
questionId: 'org_industry',
value: profile.industry,
})
}
// businessModel
if (profile.businessModel) {
answers.push({
questionId: 'org_business_model',
value: profile.businessModel,
})
}
// dpoName -> org_has_dsb
if (profile.dpoName && profile.dpoName.trim() !== '') {
answers.push({
questionId: 'org_has_dsb',
value: true,
})
}
// usesAI -> proc_ai_usage
if (profile.usesAI === true) {
// We don't know which specific AI type, so just mark as "generativ" as a default
answers.push({
questionId: 'proc_ai_usage',
value: ['generativ'],
})
} else if (profile.usesAI === false) {
answers.push({
questionId: 'proc_ai_usage',
value: ['keine'],
})
}
// offerings -> prod_type mapping
if (profile.offerings && profile.offerings.length > 0) {
const prodTypes: string[] = []
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
prodTypes.push('webapp')
}
if (
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
) {
prodTypes.push('mobile')
}
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
prodTypes.push('saas')
}
if (
offeringsLower.some(
(o) => o.includes('onpremise') || o.includes('on-premise')
)
) {
prodTypes.push('onpremise')
}
if (offeringsLower.some((o) => o.includes('api'))) {
prodTypes.push('api')
}
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
prodTypes.push('iot')
}
if (
offeringsLower.some(
(o) => o.includes('beratung') || o.includes('consulting')
)
) {
prodTypes.push('beratung')
}
if (
offeringsLower.some(
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
)
) {
prodTypes.push('handel')
}
if (prodTypes.length > 0) {
answers.push({
questionId: 'prod_type',
value: prodTypes,
})
}
}
return answers
}
/**
* Prefill scope answers from VVT profiling answers
*/
export function prefillFromVVTAnswers(
vvtAnswers: Record<string, unknown>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: VVT question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToVVTQuestion) {
reverseMap[q.mapsToVVTQuestion] = q.id
}
}
}
// Map VVT answers to scope answers
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
const scopeQuestionId = reverseMap[vvtQuestionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: vvtValue,
})
}
}
return answers
}
/**
* Prefill scope answers from Loeschfristen profiling answers
*/
export function prefillFromLoeschfristenAnswers(
lfAnswers: Array<{ questionId: string; value: unknown }>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: LF question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToLFQuestion) {
reverseMap[q.mapsToLFQuestion] = q.id
}
}
}
// Map LF answers to scope answers
for (const lfAnswer of lfAnswers) {
const scopeQuestionId = reverseMap[lfAnswer.questionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: lfAnswer.value,
})
}
}
return answers
}
/**
* Export scope answers in VVT format
*/
export function exportToVVTAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const vvtAnswers: Record<string, unknown> = {}
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToVVTQuestion) {
vvtAnswers[question.mapsToVVTQuestion] = answer.value
}
}
return vvtAnswers
}
/**
* Export scope answers in Loeschfristen format
*/
export function exportToLoeschfristenAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Array<{ questionId: string; value: unknown }> {
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToLFQuestion) {
lfAnswers.push({
questionId: question.mapsToLFQuestion,
value: answer.value,
})
}
}
return lfAnswers
}
/**
* Export scope answers for TOM generator
*/
export function exportToTOMProfile(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const tomProfile: Record<string, unknown> = {}
// Get answer values
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
// Map relevant scope answers to TOM profile fields
tomProfile.industry = getVal('org_industry')
tomProfile.employeeCount = getVal('org_employee_count')
tomProfile.hasDataMinors = getVal('data_minors')
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
? (getVal('data_art9') as string[]).length > 0
: false
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
? !(getVal('proc_ai_usage') as string[]).includes('keine')
: false
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
tomProfile.hasTraining = getVal('proc_training')
return tomProfile
}
/**
* Check if a block is complete (all required questions answered)
*/
export function isBlockComplete(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): boolean {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return false
const requiredQuestions = block.questions.filter((q) => q.required)
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
}
/**
* Get progress for a specific block (0-100)
*/
export function getBlockProgress(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): number {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return 0
const requiredQuestions = block.questions.filter((q) => q.required)
if (requiredQuestions.length === 0) return 100
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
const answeredCount = requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
return Math.round((answeredCount / requiredQuestions.length) * 100)
}
/**
* Get total progress across all blocks (0-100)
*/
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
let totalRequired = 0
let totalAnswered = 0
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
for (const block of SCOPE_QUESTION_BLOCKS) {
const requiredQuestions = block.questions.filter((q) => q.required)
totalRequired += requiredQuestions.length
totalAnswered += requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
}
if (totalRequired === 0) return 100
return Math.round((totalAnswered / totalRequired) * 100)
}
/**
* Get answer value for a specific question
*/
export function getAnswerValue(
answers: ScopeProfilingAnswer[],
questionId: string
): unknown {
const answer = answers.find((a) => a.questionId === questionId)
return answer?.value
}
/**
* Get all questions as a flat array
*/
export function getAllQuestions(): ScopeProfilingQuestion[] {
return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions)
}