/** * Integration Tests: Company Profile → Compliance Scope → VVT Generator * * Tests the complete data pipeline from Company Profile master data * through the Compliance Scope Engine to VVT activity generation. */ import { describe, it, expect } from 'vitest' import { prefillFromCompanyProfile, exportToVVTAnswers, getAutoFilledScoringAnswers, SCOPE_QUESTION_BLOCKS, } from '../compliance-scope-profiling' import { generateActivities, PROFILING_QUESTIONS, DEPARTMENT_DATA_CATEGORIES, SCOPE_PREFILLED_VVT_QUESTIONS, } from '../vvt-profiling' import type { ScopeProfilingAnswer } from '../compliance-scope-types' // Helper function ans(questionId: string, value: unknown): ScopeProfilingAnswer { return { questionId, value } as ScopeProfilingAnswer } // ============================================================================= // 1. Company Profile → Scope Prefill // ============================================================================= describe('CompanyProfile → Scope prefill', () => { it('prefills org_has_dsb when dpoName is set', () => { const profile = { dpoName: 'Max Mustermann' } as any const answers = prefillFromCompanyProfile(profile) expect(answers.find((a) => a.questionId === 'org_has_dsb')?.value).toBe(true) }) it('does NOT prefill org_has_dsb when dpoName is empty', () => { const profile = { dpoName: '' } as any const answers = prefillFromCompanyProfile(profile) expect(answers.find((a) => a.questionId === 'org_has_dsb')).toBeUndefined() }) it('maps offerings to prod_type correctly', () => { const profile = { offerings: ['WebApp', 'SaaS', 'API'] } as any const answers = prefillFromCompanyProfile(profile) const prodType = answers.find((a) => a.questionId === 'prod_type') expect(prodType?.value).toEqual(expect.arrayContaining(['webapp', 'saas', 'api'])) }) it('detects webshop in offerings', () => { const profile = { offerings: ['Webshop'] } as any const answers = prefillFromCompanyProfile(profile) expect(answers.find((a) => a.questionId === 'prod_webshop')?.value).toBe(true) }) it('returns empty array when profile has no relevant data', () => { const profile = {} as any const answers = prefillFromCompanyProfile(profile) expect(answers).toEqual([]) }) }) describe('CompanyProfile → Scope scoring answers', () => { it('maps employeeCount to org_employee_count', () => { const profile = { employeeCount: '50-249' } as any const answers = getAutoFilledScoringAnswers(profile) expect(answers.find((a) => a.questionId === 'org_employee_count')?.value).toBe('50-249') }) it('maps industry to org_industry', () => { const profile = { industry: ['IT & Software', 'Finanzdienstleistungen'] } as any const answers = getAutoFilledScoringAnswers(profile) expect(answers.find((a) => a.questionId === 'org_industry')?.value).toBe( 'IT & Software, Finanzdienstleistungen' ) }) it('maps annualRevenue to org_annual_revenue', () => { const profile = { annualRevenue: '1-10M' } as any const answers = getAutoFilledScoringAnswers(profile) expect(answers.find((a) => a.questionId === 'org_annual_revenue')?.value).toBe('1-10M') }) it('maps businessModel to org_business_model', () => { const profile = { businessModel: 'B2B' } as any const answers = getAutoFilledScoringAnswers(profile) expect(answers.find((a) => a.questionId === 'org_business_model')?.value).toBe('B2B') }) }) // ============================================================================= // 2. Scope → VVT Answer Mapping (exportToVVTAnswers) // ============================================================================= describe('Scope → VVT answer export', () => { it('maps scope questions with mapsToVVTQuestion property', () => { // Block 9: dk_dept_hr maps to dept_hr_categories const scopeAnswers: ScopeProfilingAnswer[] = [ ans('dk_dept_hr', ['NAME', 'SALARY_DATA', 'HEALTH_DATA']), ] const vvtAnswers = exportToVVTAnswers(scopeAnswers) expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'SALARY_DATA', 'HEALTH_DATA']) }) it('maps multiple department data categories', () => { const scopeAnswers: ScopeProfilingAnswer[] = [ ans('dk_dept_hr', ['NAME', 'BANK_ACCOUNT']), ans('dk_dept_finance', ['INVOICE_DATA', 'TAX_ID']), ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA']), ] const vvtAnswers = exportToVVTAnswers(scopeAnswers) expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'BANK_ACCOUNT']) expect(vvtAnswers.dept_finance_categories).toEqual(['INVOICE_DATA', 'TAX_ID']) expect(vvtAnswers.dept_marketing_categories).toEqual(['EMAIL', 'TRACKING_DATA']) }) it('ignores scope questions without mapsToVVTQuestion', () => { const scopeAnswers: ScopeProfilingAnswer[] = [ ans('vvt_has_vvt', true), // No mapsToVVTQuestion property ] const vvtAnswers = exportToVVTAnswers(scopeAnswers) expect(Object.keys(vvtAnswers)).toHaveLength(0) }) it('handles empty scope answers', () => { const vvtAnswers = exportToVVTAnswers([]) expect(vvtAnswers).toEqual({}) }) }) // ============================================================================= // 3. Scope → VVT Profiling Prefill // Note: prefillFromScopeAnswers() uses dynamic require('./compliance-scope-profiling') // which doesn't resolve in vitest. We test the same pipeline by calling // exportToVVTAnswers() directly (which is what prefillFromScopeAnswers wraps). // ============================================================================= describe('Scope → VVT Profiling Prefill (via exportToVVTAnswers)', () => { it('converts scope answers to VVT ProfilingAnswers format', () => { const scopeAnswers: ScopeProfilingAnswer[] = [ ans('dk_dept_hr', ['NAME', 'HEALTH_DATA']), ans('dk_dept_finance', ['BANK_ACCOUNT']), ] const exported = exportToVVTAnswers(scopeAnswers) // Same transformation as prefillFromScopeAnswers const profiling: Record = {} for (const [key, value] of Object.entries(exported)) { if (value !== undefined && value !== null) profiling[key] = value } expect(profiling.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA']) expect(profiling.dept_finance_categories).toEqual(['BANK_ACCOUNT']) }) it('filters out null/undefined values', () => { const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', null)] const exported = exportToVVTAnswers(scopeAnswers) const profiling: Record = {} for (const [key, value] of Object.entries(exported)) { if (value !== undefined && value !== null) profiling[key] = value } expect(profiling.dept_hr_categories).toBeUndefined() }) }) // ============================================================================= // 4. VVT Generator — generateActivities // ============================================================================= describe('generateActivities', () => { it('always generates 4 IT baseline activities', () => { const result = generateActivities({}) const names = result.generatedActivities.map((a) => a.name) expect(result.generatedActivities.length).toBeGreaterThanOrEqual(4) // IT baselines are always added const itTemplates = result.generatedActivities.filter( (a) => a.businessFunction === 'it_operations' ) expect(itTemplates.length).toBeGreaterThanOrEqual(4) }) it('triggers HR templates when dept_hr=true', () => { const result = generateActivities({ dept_hr: true }) const hrActivities = result.generatedActivities.filter( (a) => a.businessFunction === 'hr' ) expect(hrActivities.length).toBeGreaterThanOrEqual(3) // mitarbeiter, gehalt, zeiterfassung }) it('triggers finance templates when dept_finance=true', () => { const result = generateActivities({ dept_finance: true }) const financeActivities = result.generatedActivities.filter( (a) => a.businessFunction === 'finance' ) expect(financeActivities.length).toBeGreaterThanOrEqual(2) // buchhaltung, zahlungsverkehr }) it('enriches activities with US cloud third-country transfer', () => { const result = generateActivities({ dept_hr: true, transfer_cloud_us: true }) // Every activity should have a US third-country transfer for (const activity of result.generatedActivities) { expect(activity.thirdCountryTransfers.some((t) => t.country === 'US')).toBe(true) } }) it('adds HEALTH_DATA to HR activities when data_health=true', () => { const result = generateActivities({ dept_hr: true, data_health: true }) const hrActivities = result.generatedActivities.filter( (a) => a.businessFunction === 'hr' ) expect(hrActivities.length).toBeGreaterThan(0) for (const hr of hrActivities) { expect(hr.personalDataCategories).toContain('HEALTH_DATA') } }) it('calculates Art. 30 Abs. 5 exemption correctly', () => { // < 250 employees, no special categories → exempt const result1 = generateActivities({ org_employees: 50 }) expect(result1.art30Abs5Exempt).toBe(true) // >= 250 employees → not exempt const result2 = generateActivities({ org_employees: 500 }) expect(result2.art30Abs5Exempt).toBe(false) // < 250 but with special categories → not exempt const result3 = generateActivities({ org_employees: 50, data_health: true }) expect(result3.art30Abs5Exempt).toBe(false) }) it('generates unique VVT IDs for all activities', () => { const result = generateActivities({ dept_hr: true, dept_finance: true, dept_sales: true, dept_marketing: true, }) const ids = result.generatedActivities.map((a) => a.vvtId) const uniqueIds = new Set(ids) expect(uniqueIds.size).toBe(ids.length) }) it('calculates coverage score > 0 for template-generated activities', () => { const result = generateActivities({ dept_hr: true }) expect(result.coverageScore).toBeGreaterThan(0) }) }) // ============================================================================= // 5. Full Pipeline: Company Profile → Scope → VVT // ============================================================================= describe('Full Pipeline: CompanyProfile → Scope → VVT Generation', () => { // Helper: replicate what prefillFromScopeAnswers does (avoiding dynamic require) function scopeToProfilingAnswers( scopeAnswers: ScopeProfilingAnswer[] ): Record { const exported = exportToVVTAnswers(scopeAnswers) const profiling: Record = {} for (const [key, value] of Object.entries(exported)) { if (value !== undefined && value !== null) { profiling[key] = value as string | string[] | number | boolean } } return profiling } it('complete flow: profile with DSB → scope prefill → VVT generation', () => { // Step 1: Company Profile const profile = { dpoName: 'Dr. Datenschutz', employeeCount: '50-249', industry: ['IT & Software'], offerings: ['WebApp', 'SaaS'], } as any // Step 2: Prefill scope from profile const profileAnswers = prefillFromCompanyProfile(profile) const scoringAnswers = getAutoFilledScoringAnswers(profile) // Simulate user answering scope questions + auto-prefilled from profile const userAnswers: ScopeProfilingAnswer[] = [ // Block 8: departments ans('vvt_departments', ['personal', 'finanzen', 'it']), // Block 9: data categories per department ans('dk_dept_hr', ['NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA']), ans('dk_dept_finance', ['NAME', 'BANK_ACCOUNT', 'INVOICE_DATA', 'TAX_ID']), ans('dk_dept_it', ['USER_ACCOUNTS', 'LOG_DATA', 'DEVICE_DATA']), // Block 2: data types ans('data_art9', true), ans('data_minors', false), ] const allScopeAnswers = [...profileAnswers, ...scoringAnswers, ...userAnswers] // Step 3: Export to VVT format const vvtAnswers = exportToVVTAnswers(allScopeAnswers) expect(vvtAnswers.dept_hr_categories).toEqual([ 'NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA', ]) expect(vvtAnswers.dept_finance_categories).toEqual([ 'NAME', 'BANK_ACCOUNT', 'INVOICE_DATA', 'TAX_ID', ]) // Step 4: Prefill VVT profiling from scope (via direct export) const profilingAnswers = scopeToProfilingAnswers(allScopeAnswers) // Verify data survived the transformation expect(profilingAnswers.dept_hr_categories).toEqual([ 'NAME', 'ADDRESS', 'SALARY_DATA', 'HEALTH_DATA', ]) // Step 5: Generate VVT activities // Add department triggers that match Block 8 selections profilingAnswers.dept_hr = true profilingAnswers.dept_finance = true const result = generateActivities(profilingAnswers) // Verify activities were generated expect(result.generatedActivities.length).toBeGreaterThan(4) // 4 IT baseline + HR + Finance // Verify HR activities exist const hrActivities = result.generatedActivities.filter( (a) => a.businessFunction === 'hr' ) expect(hrActivities.length).toBeGreaterThanOrEqual(3) // Verify finance activities exist const financeActivities = result.generatedActivities.filter( (a) => a.businessFunction === 'finance' ) expect(financeActivities.length).toBeGreaterThanOrEqual(2) }) it('end-to-end: departments selected in scope generate correct VVT activities', () => { // Simulate a complete scope session with department selections const scopeAnswers: ScopeProfilingAnswer[] = [ // Block 2: data_art9 maps to data_health in VVT ans('data_art9', true), // Block 4: tech_third_country maps to transfer_cloud_us ans('tech_third_country', true), // Block 8: departments ans('vvt_departments', ['personal', 'marketing', 'kundenservice']), // Block 9: per-department data categories ans('dk_dept_hr', ['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS']), ans('dk_dept_marketing', ['EMAIL', 'TRACKING_DATA', 'CONSENT_DATA']), ans('dk_dept_support', ['NAME', 'TICKET_DATA', 'COMMUNICATION_DATA']), ] // Transform to VVT answers const vvtAnswers = exportToVVTAnswers(scopeAnswers) // Verify Block 9 data categories are mapped correctly expect(vvtAnswers.dept_hr_categories).toEqual(['NAME', 'HEALTH_DATA', 'RELIGIOUS_BELIEFS']) expect(vvtAnswers.dept_marketing_categories).toEqual([ 'EMAIL', 'TRACKING_DATA', 'CONSENT_DATA', ]) expect(vvtAnswers.dept_support_categories).toEqual([ 'NAME', 'TICKET_DATA', 'COMMUNICATION_DATA', ]) // Verify the full pipeline using direct export const profilingAnswers = scopeToProfilingAnswers(scopeAnswers) expect(profilingAnswers.dept_hr_categories).toBeDefined() expect(profilingAnswers.dept_marketing_categories).toBeDefined() expect(profilingAnswers.dept_support_categories).toBeDefined() }) }) // ============================================================================= // 6. DEPARTMENT_DATA_CATEGORIES Integrity // ============================================================================= describe('DEPARTMENT_DATA_CATEGORIES consistency', () => { it('all 12 departments are defined', () => { const expected = [ 'dept_hr', 'dept_recruiting', 'dept_finance', 'dept_sales', 'dept_marketing', 'dept_support', 'dept_it', 'dept_recht', 'dept_produktion', 'dept_logistik', 'dept_einkauf', 'dept_facility', ] for (const dept of expected) { expect(DEPARTMENT_DATA_CATEGORIES[dept]).toBeDefined() expect(DEPARTMENT_DATA_CATEGORIES[dept].categories.length).toBeGreaterThan(0) } }) it('every department has a label and icon', () => { for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) { expect(dept.label).toBeTruthy() expect(dept.icon).toBeTruthy() } }) it('every category has id and label', () => { for (const [, dept] of Object.entries(DEPARTMENT_DATA_CATEGORIES)) { for (const cat of dept.categories) { expect(cat.id).toBeTruthy() expect(cat.label).toBeTruthy() expect(cat.info).toBeTruthy() } } }) it('Art. 9 categories are correctly flagged', () => { const art9Categories = [ { dept: 'dept_hr', id: 'HEALTH_DATA' }, { dept: 'dept_hr', id: 'RELIGIOUS_BELIEFS' }, { dept: 'dept_recruiting', id: 'HEALTH_DATA' }, { dept: 'dept_recht', id: 'CRIMINAL_DATA' }, { dept: 'dept_produktion', id: 'HEALTH_DATA' }, { dept: 'dept_facility', id: 'HEALTH_DATA' }, ] for (const { dept, id } of art9Categories) { const cat = DEPARTMENT_DATA_CATEGORIES[dept].categories.find((c) => c.id === id) expect(cat?.isArt9).toBe(true) } }) }) // ============================================================================= // 7. Block 9 ↔ VVT Mapping Integrity // ============================================================================= describe('Block 9 Scope ↔ VVT question mapping', () => { it('every Block 9 question has mapsToVVTQuestion', () => { const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail') expect(block9).toBeDefined() for (const q of block9!.questions) { expect(q.mapsToVVTQuestion).toBeTruthy() expect(q.mapsToVVTQuestion).toMatch(/^dept_\w+_categories$/) } }) it('Block 9 question options match DEPARTMENT_DATA_CATEGORIES', () => { const block9 = SCOPE_QUESTION_BLOCKS.find((b) => b.id === 'datenkategorien_detail') expect(block9).toBeDefined() // dk_dept_hr should have same options as DEPARTMENT_DATA_CATEGORIES.dept_hr const hrQuestion = block9!.questions.find((q) => q.id === 'dk_dept_hr') expect(hrQuestion).toBeDefined() const expectedIds = DEPARTMENT_DATA_CATEGORIES.dept_hr.categories.map((c) => c.id) const actualIds = hrQuestion!.options!.map((o) => o.value) expect(actualIds).toEqual(expectedIds) }) it('SCOPE_PREFILLED_VVT_QUESTIONS lists all cross-module questions', () => { expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('org_industry') expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('dept_hr') expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('data_health') expect(SCOPE_PREFILLED_VVT_QUESTIONS).toContain('transfer_cloud_us') expect(SCOPE_PREFILLED_VVT_QUESTIONS.length).toBeGreaterThanOrEqual(15) }) }) // ============================================================================= // 8. Edge Cases // ============================================================================= describe('Edge cases', () => { it('generateActivities with no answers still produces IT baselines', () => { const result = generateActivities({}) expect(result.generatedActivities.length).toBe(4) // 4 IT baselines expect(result.art30Abs5Exempt).toBe(true) // 0 employees, no special categories }) it('same template triggered by multiple questions is only generated once', () => { const result = generateActivities({ dept_sales: true, // triggers sales-kundenverwaltung sys_crm: true, // also triggers sales-kundenverwaltung }) const salesKunden = result.generatedActivities.filter((a) => a.name.toLowerCase().includes('kundenverwaltung') ) // Should be deduplicated (Set-based triggeredIds) expect(salesKunden.length).toBe(1) }) it('empty department category selections produce valid but empty mappings', () => { const scopeAnswers: ScopeProfilingAnswer[] = [ans('dk_dept_hr', [])] const vvtAnswers = exportToVVTAnswers(scopeAnswers) expect(vvtAnswers.dept_hr_categories).toEqual([]) }) })