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
96 lines
3.1 KiB
Python
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])
|