feat(founding-wizard): Gründungs-Wizard für 2-Mann GmbH + 14 Notar-Templates
[migration-approved]
Templates (Migrations 123-136):
- 123 GO-GF (Geschäftsordnung Geschäftsführung)
- 124 SHA (Shareholders' Agreement, 56 Platzhalter)
- 125 Satzung (Articles of Association mit UG-Variante)
- 126 GF-Dienstvertrag (Trennungsprinzip Organ/Anstellung)
- 127 Arbeitsvertrag (AGG-neutral, NachwG, eAU)
- 128 Gesellschafterliste (§ 40 GmbHG)
- 129 GF-Bestellungsbeschluss (mit § 6 Abs. 2 Versicherung)
- 130 HRB-Anmeldung (§§ 7, 8, 39 GmbHG, § 12 HGB)
- 131 IP-Assignment Agreement (Gründer→GmbH)
- 132 Term Sheet (Pre-Seed/Seed VC-Standard)
- 133 Wandeldarlehensvertrag (Convertible Loan)
- 134 Beteiligungsvertrag (Subscription Agreement)
- 135 ESOP/VSOP-Plan (3 Varianten)
- 136 Cap Table
Kategorisierung (Migrations 137-138):
- ALTER TABLE compliance_legal_templates ADD lifecycle_stage TEXT[],
functional_category TEXT (mit CHECK Constraints + GIN-Index)
- Backfill aller 105 Templates: lifecycle_stage (pre_founding|founding|
startup|kmu|konzern) + functional_category (founding_legal|employment|
investor_funding|...)
Backend Founding-Wizard Service:
- template_renderer.py: Handlebars-light ({{VAR}}, {{#IF FLAG}}...{{/IF}})
- wizard_to_context.py: Mapping Wizard-State → SCREAMING_SNAKE_CASE Vars
- markdown_to_docx.py: Markdown → DOCX via python-docx
- founding_wizard_routes.py: POST /v1/founding-wizard/generate
→ liefert base64-DOCX-Files für ausgewählte Templates
Frontend Founding-Wizard (/sdk/founding-wizard):
- 8-Step Wizard (Basics, Gesellschafter, GF, Kapital, Notar, SHA, GF-Verträge, Generate)
- useFoundingWizardForm Hook mit localStorage-Persistenz
- TypeScript Code-Registry (template-categories.ts) als Backup zur DB
- Word-Download via data:URLs (base64)
Tests:
- 20 Unit-Tests grün (Renderer, Context-Mapping, DOCX-Conversion)
- Playwright E2E-Test mit 2-Mann GmbH (Benjamin + Sharang) Test-Daten
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Playwright E2E-Test: Founding-Wizard mit 2-Mann GmbH (Benjamin Bönisch + Sharang Parnerkar).
|
||||
*
|
||||
* Test-Flow:
|
||||
* 1. Lokale Dev-URL aufrufen
|
||||
* 2. Wizard durch alle 8 Steps befüllen
|
||||
* 3. Dokumente generieren (8 Stück für Notartermin-Bundle)
|
||||
* 4. Word-Download-Links validieren
|
||||
*
|
||||
* Voraussetzung: `npm run dev` läuft auf http://localhost:3007
|
||||
* Backend ist erreichbar (mit Migration 137 + 138 + Templates 123–136)
|
||||
*
|
||||
* Ausführen:
|
||||
* cd admin-compliance
|
||||
* npx playwright test tests/playwright/founding-wizard/
|
||||
*/
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
const BASE_URL = process.env.WIZARD_URL || 'http://localhost:3007/sdk/founding-wizard'
|
||||
|
||||
const TEST_DATA = {
|
||||
basics: {
|
||||
company_name: 'Breakpilot GmbH',
|
||||
company_seat: 'Bietigheim-Bissingen',
|
||||
company_address: 'Hauptstraße 1, 74321 Bietigheim-Bissingen',
|
||||
industry: 'Software / KI / SaaS',
|
||||
purpose: 'die Entwicklung, Bereitstellung und der Vertrieb von Softwarelösungen, Plattformen und IT-Dienstleistungen im Bereich der Künstlichen Intelligenz sowie compliance-bezogener Datenverarbeitungssysteme',
|
||||
bullets: [
|
||||
'a) Entwicklung, Programmierung und Betrieb von KI-gestützter Compliance-Software',
|
||||
'b) Bereitstellung von datenschutzkonformen SaaS-Lösungen für Unternehmen',
|
||||
'c) Beratungs- und Integrationsleistungen im Compliance-Umfeld',
|
||||
],
|
||||
},
|
||||
notar: {
|
||||
name: 'Dr. Müller',
|
||||
place: 'Stuttgart',
|
||||
address: 'Königstraße 1, 70173 Stuttgart',
|
||||
date: '2026-06-15',
|
||||
},
|
||||
gesellschafter: [
|
||||
{
|
||||
name: 'Benjamin Bönisch',
|
||||
birthdate: '1980-03-15',
|
||||
address: 'Hauptstraße 1, 74321 Bietigheim-Bissingen',
|
||||
email: 'benjamin@breakpilot.ai',
|
||||
nennbetrag: 12500,
|
||||
is_gf: true,
|
||||
role: 'CEO',
|
||||
},
|
||||
{
|
||||
name: 'Sharang Parnerkar',
|
||||
birthdate: '1985-09-22',
|
||||
address: 'Hauptstraße 2, 74321 Bietigheim-Bissingen',
|
||||
email: 'sharang@breakpilot.ai',
|
||||
nennbetrag: 12500,
|
||||
is_gf: true,
|
||||
role: 'CTO',
|
||||
},
|
||||
],
|
||||
stammkapital: 25000,
|
||||
}
|
||||
|
||||
test.describe('Founding Wizard — 2-Mann GmbH', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Clear localStorage to start fresh
|
||||
await page.goto(BASE_URL)
|
||||
await page.evaluate(() => localStorage.clear())
|
||||
await page.reload()
|
||||
})
|
||||
|
||||
test('füllt komplette 2-Mann GmbH aus und generiert Notartermin-Bundle', async ({ page }) => {
|
||||
await page.goto(BASE_URL)
|
||||
await expect(page.getByTestId('founding-wizard')).toBeVisible()
|
||||
|
||||
// STEP 1: Basics
|
||||
await expect(page.getByTestId('step-content-1')).toBeVisible()
|
||||
await page.getByTestId('company-name').fill(TEST_DATA.basics.company_name)
|
||||
await page.getByTestId('legal-form').selectOption('GmbH')
|
||||
await page.getByTestId('company-seat').fill(TEST_DATA.basics.company_seat)
|
||||
await page.getByTestId('company-address').fill(TEST_DATA.basics.company_address)
|
||||
await page.getByTestId('industry').fill(TEST_DATA.basics.industry)
|
||||
await page.getByTestId('company-purpose').fill(TEST_DATA.basics.purpose)
|
||||
await page.getByTestId('company-purpose-bullets').fill(TEST_DATA.basics.bullets.join('\n'))
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 2: Gesellschafter
|
||||
await expect(page.getByTestId('step-content-2')).toBeVisible()
|
||||
for (const gs of TEST_DATA.gesellschafter) {
|
||||
await page.getByTestId('gs-name').fill(gs.name)
|
||||
await page.getByTestId('gs-birthdate').fill(gs.birthdate)
|
||||
await page.getByTestId('gs-address').fill(gs.address)
|
||||
await page.getByTestId('gs-email').fill(gs.email)
|
||||
await page.getByTestId('gs-nennbetrag').fill(String(gs.nennbetrag))
|
||||
await page.getByTestId('gs-role').fill(gs.role)
|
||||
// is_gf bereits default true, nichts zu tun
|
||||
await page.getByTestId('add-gesellschafter').click()
|
||||
}
|
||||
await expect(page.getByTestId('gs-row-1')).toContainText('Benjamin Bönisch')
|
||||
await expect(page.getByTestId('gs-row-2')).toContainText('Sharang Parnerkar')
|
||||
await expect(page.getByTestId('gs-total')).toContainText('25.000')
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 3: GF-Assignment (beide bereits GF aus Step 2)
|
||||
await expect(page.getByTestId('step-content-3')).toBeVisible()
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 4: Kapital
|
||||
await expect(page.getByTestId('step-content-4')).toBeVisible()
|
||||
await expect(page.getByTestId('stammkapital')).toHaveValue('25000')
|
||||
await page.getByTestId('einlage-method').selectOption('Geld')
|
||||
await page.getByTestId('einlage-quote').fill('50')
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 5: Notar
|
||||
await expect(page.getByTestId('step-content-5')).toBeVisible()
|
||||
await page.getByTestId('notary-name').fill(TEST_DATA.notar.name)
|
||||
await page.getByTestId('notary-place').fill(TEST_DATA.notar.place)
|
||||
await page.getByTestId('notary-address').fill(TEST_DATA.notar.address)
|
||||
await page.getByTestId('notarial-date').fill(TEST_DATA.notar.date)
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 6: SHA-Optionen
|
||||
await expect(page.getByTestId('step-content-6')).toBeVisible()
|
||||
await expect(page.getByTestId('has-sha')).toBeChecked()
|
||||
await expect(page.getByTestId('vesting-months')).toHaveValue('48')
|
||||
await expect(page.getByTestId('drag-along-pct')).toHaveValue('75')
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 7: GF-Verträge (für beide Founders)
|
||||
await expect(page.getByTestId('step-content-7')).toBeVisible()
|
||||
// GF-Contracts werden mit Defaults erzeugt sobald GFs definiert sind -
|
||||
// wir editieren die Gehälter
|
||||
const contracts = page.locator('[data-testid^="contract-"]')
|
||||
const count = await contracts.count()
|
||||
expect(count).toBe(2)
|
||||
await page.getByTestId('next-step').click()
|
||||
|
||||
// STEP 8: Generate
|
||||
await expect(page.getByTestId('step-content-8')).toBeVisible()
|
||||
await expect(page.getByTestId('generate-summary')).toContainText('Breakpilot GmbH')
|
||||
await expect(page.getByTestId('generate-summary')).toContainText('Bietigheim-Bissingen')
|
||||
await expect(page.getByTestId('generate-summary')).toContainText('25.000')
|
||||
|
||||
// Notartermin-Bundle auswählen
|
||||
await page.getByTestId('select-notary-bundle').click()
|
||||
// Check that bundle items are selected
|
||||
await expect(page.getByTestId('doc-articles_of_association')).toBeChecked()
|
||||
await expect(page.getByTestId('doc-sha')).toBeChecked()
|
||||
await expect(page.getByTestId('doc-gesellschafterliste')).toBeChecked()
|
||||
await expect(page.getByTestId('doc-managing_director_employment_contract')).toBeChecked()
|
||||
|
||||
// Generate
|
||||
await page.getByTestId('generate-docs').click()
|
||||
|
||||
// Warten auf Generierung (max 30s)
|
||||
await expect(page.getByTestId('generated-docs')).toBeVisible({ timeout: 30000 })
|
||||
|
||||
// Mindestens 8 Dokumente sollten erscheinen (für 2 Founders evtl. doppelt: GF-Vertrag, IP-Assignment)
|
||||
const downloadLinks = page.locator('[data-testid^="download-"]')
|
||||
const linkCount = await downloadLinks.count()
|
||||
expect(linkCount).toBeGreaterThanOrEqual(8)
|
||||
|
||||
// Validiere dass download-URLs data: URLs sind (base64 DOCX)
|
||||
for (let i = 0; i < Math.min(linkCount, 3); i++) {
|
||||
const href = await downloadLinks.nth(i).getAttribute('href')
|
||||
expect(href).toMatch(/^data:application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document;base64,/)
|
||||
}
|
||||
|
||||
// Screenshot fürs Test-Artifact
|
||||
await page.screenshot({ path: 'test-results/founding-wizard-final.png', fullPage: true })
|
||||
})
|
||||
|
||||
test('zeigt Validierung wenn Pflichtfelder fehlen', async ({ page }) => {
|
||||
await page.goto(BASE_URL)
|
||||
// Next-Button sollte disabled sein wenn nichts ausgefüllt
|
||||
await expect(page.getByTestId('next-step')).toBeDisabled()
|
||||
|
||||
await page.getByTestId('company-name').fill('Test')
|
||||
// Immer noch disabled weil purpose fehlt
|
||||
await expect(page.getByTestId('next-step')).toBeDisabled()
|
||||
|
||||
await page.getByTestId('company-seat').fill('Stuttgart')
|
||||
await page.getByTestId('company-purpose').fill('Eine lange genug Beschreibung des Zwecks.')
|
||||
// Jetzt sollte er enabled sein
|
||||
await expect(page.getByTestId('next-step')).toBeEnabled()
|
||||
})
|
||||
|
||||
test('Reset löscht alle Daten', async ({ page }) => {
|
||||
await page.goto(BASE_URL)
|
||||
await page.getByTestId('company-name').fill('Wird gelöscht GmbH')
|
||||
page.on('dialog', d => d.accept())
|
||||
await page.getByTestId('reset-wizard').click()
|
||||
await expect(page.getByTestId('company-name')).toHaveValue('')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user