From 7335f64f4fb55bc1046d5f4654140acd4dafeb7c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 21 May 2026 18:49:10 +0200 Subject: [PATCH] feat(founding-wizard): Per-Person IP-Assignment + Prefill + E2E-Tests Wizard unterstuetzt jetzt 2-4 Gesellschafter mit individuellem IP-Bereich: - Pro Gruender ein IP-Assignment-Vertrag (z.B. Benjamin: Compliance+RAG; Sharang: Security+Infrastruktur). Pro GF ein eigener Dienstvertrag. - Step 1: Prefill-Button aus Unternehmensprofil + Felder Registergericht und HRB-Nr. - Step 2: Rollen-Dropdown (CEO/CTO/CFO/COO/CPO/GF/Sonstige) statt freie Texteingabe, IP-Bereiche-Textarea pro Person. Backend: - generate_documents() iteriert pro Person fuer PER_PERSON_DOCS. - _build_person_context() injiziert ASSIGNOR_*, GF_*, IP_LIST_DETAILS aus person.ip_areas. - base_context() propagiert basics.register_court und basics.hrb_number. Tests: - 30/30 Pytest gruen (6 neue: Per-Person-Context, Slug-Helper, Registergericht-Propagation). - 4 neue Playwright-E2E-Specs (hermetisch via route.fulfill, mit Console-/Page-Error-Traps): kompletter 8-Step-Flow, Prefill-Fehlerpfad, Step-Navigation/Reset, Rollen-Dropdown + IP-Areas. - Spec setzt 'bp-sdk-cookie-consent' im addInitScript damit der CookieBannerOverlay nicht die Wizard-Buttons ueberlagert. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../_components/StepBasics.tsx | 95 +++++ .../_components/StepGesellschafter.tsx | 50 ++- .../e2e/specs/founding-wizard.spec.ts | 355 ++++++++++++++++++ admin-compliance/lib/sdk/founding/types.ts | 8 + .../compliance/api/founding_wizard_routes.py | 115 +++++- .../founding_wizard/wizard_to_context.py | 7 +- .../tests/test_founding_wizard.py | 64 ++++ 7 files changed, 677 insertions(+), 17 deletions(-) create mode 100644 admin-compliance/e2e/specs/founding-wizard.spec.ts diff --git a/admin-compliance/app/sdk/founding-wizard/_components/StepBasics.tsx b/admin-compliance/app/sdk/founding-wizard/_components/StepBasics.tsx index abb07705..c8038a71 100644 --- a/admin-compliance/app/sdk/founding-wizard/_components/StepBasics.tsx +++ b/admin-compliance/app/sdk/founding-wizard/_components/StepBasics.tsx @@ -1,5 +1,6 @@ 'use client' +import { useState } from 'react' import type { FoundingWizardState } from '@/lib/sdk/founding/types' interface Props { @@ -9,8 +10,73 @@ interface Props { export function StepBasics({ state, update }: Props) { const b = state.basics + const [prefillStatus, setPrefillStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle') + + async function prefillFromCompanyProfile() { + setPrefillStatus('loading') + try { + const res = await fetch('/api/sdk/v1/company-profile', { cache: 'no-store' }) + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const payload = await res.json() + const p = payload?.profile ?? payload + if (!p || typeof p !== 'object') throw new Error('leeres Profil') + const industries = Array.isArray(p.industry) ? p.industry.filter(Boolean) : [] + const industry = industries.length > 0 + ? industries.join(', ') + : (p.industryOther || b.industry) + const address = [p.headquartersStreet, [p.headquartersZip, p.headquartersCity].filter(Boolean).join(' ')] + .filter(Boolean).join(', ') || b.company_address + const seat = p.headquartersCity || b.company_seat + // Purpose ableiten aus offerings/businessModel — Fallback wenn nichts da + const purposeBits: string[] = [] + if (p.businessModel) purposeBits.push(`Geschäftsmodell: ${p.businessModel}`) + if (Array.isArray(p.offerings) && p.offerings.length > 0) + purposeBits.push(`Leistungen: ${p.offerings.join(', ')}`) + const purpose = purposeBits.length > 0 + ? purposeBits.join('; ') + : b.company_purpose_description + update('basics', { + ...b, + company_name: p.companyName || b.company_name, + legal_form: (p.legalForm === 'UG' ? 'UG' : (p.legalForm === 'GmbH' ? 'GmbH' : b.legal_form)), + company_seat: seat, + company_address: address, + industry, + company_purpose_description: b.company_purpose_description.trim() === '' ? purpose : b.company_purpose_description, + }) + setPrefillStatus('success') + } catch (err) { + console.error('[founding-wizard] prefill failed', err) + setPrefillStatus('error') + } + } + return (
+
+

+ Stammdaten der Gesellschaft. Pflicht für Satzung, HRB-Anmeldung und SHA. +

+ +
+ {prefillStatus === 'success' && ( +
+ Daten aus Unternehmensprofil übernommen. Bitte prüfen und ergänzen. +
+ )} + {prefillStatus === 'error' && ( +
+ Konnte Unternehmensprofil nicht laden — bitte Felder manuell ausfüllen. +
+ )} +
@@ -78,6 +144,35 @@ export function StepBasics({ state, update }: Props) { className="w-full px-3 py-2 border rounded-lg" />
+
+ + update('basics', { ...b, register_court: e.target.value })} + placeholder="z.B. Amtsgericht Stuttgart" + className="w-full px-3 py-2 border rounded-lg" + /> +

+ Zuständiges Amtsgericht für HRB-Eintragung +

+
+
+ + update('basics', { ...b, hrb_number: e.target.value })} + placeholder="z.B. HRB 12345 (leer falls noch nicht eingetragen)" + className="w-full px-3 py-2 border rounded-lg" + /> +
diff --git a/admin-compliance/app/sdk/founding-wizard/_components/StepGesellschafter.tsx b/admin-compliance/app/sdk/founding-wizard/_components/StepGesellschafter.tsx index 8d310716..b7782582 100644 --- a/admin-compliance/app/sdk/founding-wizard/_components/StepGesellschafter.tsx +++ b/admin-compliance/app/sdk/founding-wizard/_components/StepGesellschafter.tsx @@ -14,7 +14,7 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf const [form, setForm] = useState({ name: '', geburtsdatum: '', adresse: '', email: '', nennbetrag_eur: 12500, is_geschaeftsfuehrer: true, internal_role: '', - has_academic_background: false, + has_academic_background: false, ip_areas: '', }) const totalNennbetrag = state.gesellschafter.reduce((s, g) => s + g.nennbetrag_eur, 0) @@ -22,6 +22,8 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf const handleAdd = () => { if (!form.name.trim()) return + const ip_areas = form.ip_areas + .split('\n').map(s => s.trim()).filter(Boolean) addGesellschafter({ rolle: 'founder', name: form.name, @@ -32,9 +34,10 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf is_geschaeftsfuehrer: form.is_geschaeftsfuehrer, internal_role: form.internal_role || undefined, has_academic_background: form.has_academic_background, + ip_areas: ip_areas.length > 0 ? ip_areas : undefined, }) setForm({ name: '', geburtsdatum: '', adresse: '', email: '', nennbetrag_eur: 12500, - is_geschaeftsfuehrer: true, internal_role: '', has_academic_background: false }) + is_geschaeftsfuehrer: true, internal_role: '', has_academic_background: false, ip_areas: '' }) } return ( @@ -82,13 +85,22 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf onChange={e => setForm({ ...form, nennbetrag_eur: parseInt(e.target.value) || 0 })} className="px-3 py-2 border rounded" /> - setForm({ ...form, internal_role: e.target.value })} - className="px-3 py-2 border rounded" - /> + className="px-3 py-2 border rounded bg-white" + > + + + + + + + + + +
Akademischer Hintergrund
+
+ +