Files
breakpilot-compliance/backend-compliance/tests/test_founding_wizard.py
T
Benjamin Admin 7a5f1e48dd 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
2026-05-20 09:30:51 +02:00

220 lines
7.5 KiB
Python

"""Unit-Tests fuer den Founding-Wizard Service.
Testet:
- Template-Renderer mit verschiedenen Inputs
- Wizard-State -> Context Mapping
- Markdown -> DOCX Konvertierung
"""
from __future__ import annotations
import pytest
from compliance.services.founding_wizard.template_renderer import (
find_undefined_placeholders,
render_template,
)
from compliance.services.founding_wizard.wizard_to_context import base_context
class TestTemplateRenderer:
def test_simple_variable_substitution(self):
result = render_template("Hallo {{NAME}}!", {"NAME": "Welt"})
assert result == "Hallo Welt!"
def test_missing_variable_placeholder(self):
result = render_template("Hallo {{NAME}}!", {})
assert "[NAME fehlt]" in result
def test_if_block_truthy(self):
result = render_template(
"Start {{#IF FLAG}}drin{{/IF}} Ende",
{"FLAG": True}
)
assert result == "Start drin Ende"
def test_if_block_falsy(self):
result = render_template(
"Start {{#IF FLAG}}drin{{/IF}} Ende",
{"FLAG": False}
)
assert result == "Start Ende"
def test_if_not_block(self):
result = render_template(
"{{#IF NOT FLAG}}negiert{{/IF}}",
{"FLAG": False}
)
assert "negiert" in result
def test_truthy_int(self):
result = render_template("{{#IF N}}yes{{/IF}}", {"N": 5})
assert "yes" in result
def test_falsy_zero(self):
result = render_template("{{#IF N}}yes{{/IF}}", {"N": 0})
assert "yes" not in result
def test_truthy_list(self):
result = render_template("{{#IF L}}yes{{/IF}}", {"L": [1, 2]})
assert "yes" in result
def test_falsy_empty_list(self):
result = render_template("{{#IF L}}yes{{/IF}}", {"L": []})
assert "yes" not in result
def test_nested_if_blocks(self):
template = "{{#IF A}}A-on{{#IF B}}+B-on{{/IF}}{{/IF}}"
result = render_template(template, {"A": True, "B": True})
assert result == "A-on+B-on"
def test_find_undefined_placeholders(self):
template = "{{X}} {{Y}} {{#IF Z}}.{{/IF}}"
undefined = find_undefined_placeholders(template, {"X": "1"})
assert "Y" in undefined
assert "Z" in undefined
assert "X" not in undefined
class TestWizardToContext:
def _basic_state(self) -> dict:
return {
"basics": {
"company_name": "Test GmbH",
"legal_form": "GmbH",
"company_seat": "Stuttgart",
"company_address": "Königstraße 1, 70173 Stuttgart",
"company_purpose_description": "Test purpose",
"company_purpose_bullets": ["a) Test", "b) Test 2"],
"industry": "SaaS",
"business_year": "Kalenderjahr",
"has_research_focus": True,
},
"capital": {
"stammkapital_eur": 25000,
"einlage_method": "Geld",
"einlage_quote_initial_pct": 50,
"has_sacheinlage": False,
},
"gesellschafter": [
{
"id": "g1", "anteil_nr": 1, "name": "Benjamin Bönisch",
"geburtsdatum": "1980-01-01", "adresse": "Test 1",
"nennbetrag_eur": 12500, "is_geschaeftsfuehrer": True,
"internal_role": "CEO", "rolle": "founder",
"has_academic_background": False,
},
{
"id": "g2", "anteil_nr": 2, "name": "Sharang Parnerkar",
"geburtsdatum": "1985-05-15", "adresse": "Test 2",
"nennbetrag_eur": 12500, "is_geschaeftsfuehrer": True,
"internal_role": "CTO", "rolle": "founder",
"has_academic_background": False,
},
],
"notar": {
"notary_name": "Dr. Notar",
"notary_place": "Stuttgart",
"notarial_date": "2026-06-01",
},
"sha": {
"has_sha": True,
"vesting_months": 48,
"cliff_months": 12,
"drag_along_threshold_pct": 75,
"tag_along_threshold_pct": 20,
"reserved_matters_majority_pct": 75,
"has_beirat": False,
"has_texas_shootout": False,
"has_ceo_designation": False,
},
}
def test_basics_in_context(self):
ctx = base_context(self._basic_state())
assert ctx["COMPANY_NAME"] == "Test GmbH"
assert ctx["COMPANY_LEGAL_FORM"] == "GmbH"
assert ctx["COMPANY_SEAT"] == "Stuttgart"
assert ctx["STAMMKAPITAL_EUR"] == "25.000"
def test_num_gf_2_man(self):
ctx = base_context(self._basic_state())
assert ctx["NUM_GF"] == 2
assert ctx["NUM_GF_TEXT"] == "zwei"
assert ctx["IS_MULTI_GF"] is True
assert ctx["NUM_GF_IS_2"] is True
assert ctx["NUM_GF_GT_2"] is False
def test_parties_list_format(self):
ctx = base_context(self._basic_state())
plist = ctx["PARTIES_LIST"]
assert "Benjamin Bönisch" in plist
assert "Sharang Parnerkar" in plist
assert "a)" in plist
assert "b)" in plist
def test_flags_default(self):
ctx = base_context(self._basic_state())
assert ctx["HAS_SHA"] is True
assert ctx["HAS_RESEARCH_FOCUS"] is True
assert ctx["HAS_ACADEMIC_FOUNDER"] is False
assert ctx["HAS_BEIRAT"] is False
def test_academic_flag_detection(self):
state = self._basic_state()
state["gesellschafter"][0]["has_academic_background"] = True
ctx = base_context(state)
assert ctx["HAS_ACADEMIC_FOUNDER"] is True
class TestMarkdownToDocx:
def test_basic_conversion(self):
from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes
md = "# Titel\n\nDas ist ein Absatz.\n\n## Unterthema\n\n- Punkt 1\n- Punkt 2"
result = markdown_to_docx_bytes(md)
assert isinstance(result, bytes)
# DOCX is a ZIP file starting with PK\x03\x04
assert result[:4] == b"PK\x03\x04"
assert len(result) > 1000 # reasonable size
def test_table_conversion(self):
from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes
md = "| A | B |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |"
result = markdown_to_docx_bytes(md)
assert result[:4] == b"PK\x03\x04"
def test_bold_italic(self):
from compliance.services.founding_wizard.markdown_to_docx import markdown_to_docx_bytes
md = "Das ist **fett** und _kursiv_ und `code`."
result = markdown_to_docx_bytes(md)
assert result[:4] == b"PK\x03\x04"
class TestEndToEndRendering:
"""Test mit echtem Template-aehnlichen Markdown + 2-Mann GmbH Daten."""
def test_minimum_satzung_render(self):
template = """# Satzung der {{COMPANY_NAME}}
## § 1 Firma
(1) Die Gesellschaft führt die Firma {{COMPANY_NAME}}.
(2) Sitz ist {{COMPANY_SEAT}}.
{{#IF HAS_SHA}}
## § 5 SHA-Verweis
Es gilt das SHA.
{{/IF}}
{{#IF NOT HAS_SHA}}
## § 5 Hinweis
Kein SHA vereinbart.
{{/IF}}
"""
ctx = base_context(TestWizardToContext()._basic_state())
result = render_template(template, ctx)
assert "Test GmbH" in result
assert "Stuttgart" in result
assert "§ 5 SHA-Verweis" in result
assert "Kein SHA vereinbart" not in result