feat(founding-wizard): Per-Person IP-Assignment + Prefill + E2E-Tests
CI / loc-budget (push) Failing after 20s
CI / detect-changes (push) Successful in 12s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 19s
CI / nodejs-build (push) Successful in 3m17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 43s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped

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) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-21 18:49:10 +02:00
parent 138d9068c4
commit 7335f64f4f
7 changed files with 677 additions and 17 deletions
@@ -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"
/>
<input
<select
data-testid="gs-role"
placeholder="Interne Rolle (z.B. CEO, CTO)"
value={form.internal_role}
onChange={e => setForm({ ...form, internal_role: e.target.value })}
className="px-3 py-2 border rounded"
/>
className="px-3 py-2 border rounded bg-white"
>
<option value="">Rolle wählen</option>
<option value="CEO">CEO (Chief Executive Officer)</option>
<option value="CTO">CTO (Chief Technical Officer)</option>
<option value="CFO">CFO (Chief Financial Officer)</option>
<option value="COO">COO (Chief Operating Officer)</option>
<option value="CPO">CPO (Chief Product Officer)</option>
<option value="Geschäftsführer">Geschäftsführer (ohne Spezialisierung)</option>
<option value="Gesellschafter">Gesellschafter (kein GF)</option>
<option value="Sonstige">Sonstige</option>
</select>
<div className="flex items-center gap-2">
<input
type="checkbox"
@@ -108,6 +120,23 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf
<label className="text-sm">Akademischer Hintergrund</label>
</div>
</div>
<div className="mt-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
IP-Bereiche, die diese Person in die Gesellschaft einbringt
<span className="text-gray-400"> (optional, eine Zeile pro Bereich)</span>
</label>
<textarea
data-testid="gs-ip-areas"
value={form.ip_areas}
onChange={e => setForm({ ...form, ip_areas: e.target.value })}
rows={3}
placeholder={'z.B.\nCompliance-Engine (Quellcode + Architektur)\nRAG-Pipeline\nKonfigurationsdaten'}
className="w-full px-3 py-2 border rounded font-mono text-xs"
/>
<p className="text-xs text-gray-500 mt-1">
Bei mehreren Gründern wird pro Person ein eigener IP-Assignment-Vertrag generiert.
</p>
</div>
<button
data-testid="add-gesellschafter"
onClick={handleAdd}
@@ -139,7 +168,14 @@ export function StepGesellschafter({ state, addGesellschafter, updateGesellschaf
{state.gesellschafter.map(g => (
<tr key={g.id} className="border-t" data-testid={`gs-row-${g.anteil_nr}`}>
<td className="px-3 py-2">{g.anteil_nr}</td>
<td className="px-3 py-2 font-medium">{g.name}{g.internal_role ? ` (${g.internal_role})` : ''}</td>
<td className="px-3 py-2 font-medium">
{g.name}{g.internal_role ? ` (${g.internal_role})` : ''}
{g.ip_areas && g.ip_areas.length > 0 && (
<div className="text-xs text-gray-500 mt-0.5">
IP: {g.ip_areas.join(', ')}
</div>
)}
</td>
<td className="px-3 py-2">{g.geburtsdatum || '—'}</td>
<td className="px-3 py-2 text-right">{g.nennbetrag_eur.toLocaleString('de-DE')} </td>
<td className="px-3 py-2 text-right">{((g.nennbetrag_eur / Math.max(target, 1)) * 100).toFixed(2)}%</td>