7a5f1e48dd
[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
220 lines
7.5 KiB
Python
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
|