Files
breakpilot-compliance/backend-compliance/compliance/services/founding_wizard/template_renderer.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

96 lines
3.1 KiB
Python

"""
Handlebars-light Template-Renderer fuer die compliance_legal_templates.
Unterstuetzte Syntax:
- {{VARIABLE_NAME}} - einfache String-Substitution
- {{#IF FLAG}}...{{/IF}} - bedingter Block (truthy)
- {{#IF NOT FLAG}}...{{/IF}} - negierter bedingter Block
Bewusst minimal gehalten — keine Loops oder Verschachtelung tiefer Logik.
Komplexere Sachen werden im Context vorberechnet.
"""
from __future__ import annotations
import re
from typing import Any
# Pattern fuer {{#IF FLAG}}...{{/IF}} und {{#IF NOT FLAG}}...{{/IF}}
# Greedy / non-overlapping. Inhalt darf alles enthalten ausser einem geschlossenen {{/IF}}.
IF_BLOCK = re.compile(
r"\{\{#IF\s+(NOT\s+)?([A-Z_][A-Z0-9_]*)\}\}(.*?)\{\{/IF\}\}",
re.DOTALL,
)
VAR_PATTERN = re.compile(r"\{\{\s*([A-Z_][A-Z0-9_]*)\s*\}\}")
def _is_truthy(val: Any) -> bool:
"""Pythonische Truthiness, mit Special-Case: leeres dict/list/str = False."""
if val is None:
return False
if isinstance(val, bool):
return val
if isinstance(val, (int, float)):
return val != 0
if isinstance(val, str):
return val.strip() != "" and val.lower() not in ("false", "0", "no", "nein")
if isinstance(val, (list, dict, tuple, set)):
return len(val) > 0
return True
def render_template(template: str, context: dict[str, Any]) -> str:
"""Rendert ein Template mit dem gegebenen Kontext.
Algorithmus:
1. IF-Bloecke iterativ aufloesen (max 10 Durchlaeufe, damit Nesting funktioniert)
2. Variablen substituieren
Args:
template: Markdown-Template mit {{VAR}} und {{#IF FLAG}}...{{/IF}}
context: dict mit Variablen — Keys SCREAMING_SNAKE_CASE
Returns:
Gerendetes Markdown
"""
output = template
for _ in range(10): # max 10 Levels Nesting
def replace_if(match: re.Match[str]) -> str:
negated = bool(match.group(1))
flag_name = match.group(2)
content = match.group(3)
flag_val = context.get(flag_name)
condition = _is_truthy(flag_val)
if negated:
condition = not condition
return content if condition else ""
new_output = IF_BLOCK.sub(replace_if, output)
if new_output == output:
break
output = new_output
def replace_var(match: re.Match[str]) -> str:
name = match.group(1)
val = context.get(name)
if val is None:
# Leere Platzhalter sichtbar machen fuer Debugging
return f"[{name} fehlt]"
if isinstance(val, bool):
return "ja" if val else "nein"
return str(val)
output = VAR_PATTERN.sub(replace_var, output)
return output
def find_undefined_placeholders(template: str, context: dict[str, Any]) -> list[str]:
"""Listet alle Variablen-Platzhalter ohne Wert im Context."""
placeholders: set[str] = set()
for match in VAR_PATTERN.finditer(template):
placeholders.add(match.group(1))
for match in IF_BLOCK.finditer(template):
placeholders.add(match.group(2))
return sorted([p for p in placeholders if p not in context])