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
184 lines
6.3 KiB
Python
184 lines
6.3 KiB
Python
"""FastAPI-Route fuer den Founding-Wizard Document-Generation.
|
|
|
|
POST /v1/founding-wizard/generate
|
|
Body: FoundingWizardState (Wizard-Eingaben)
|
|
Returns: {documents: [{document_type, title, content_base64, size_bytes, ...}]}
|
|
|
|
Templates werden aus compliance_legal_templates geladen, mit dem Wizard-Context
|
|
gerendert (Handlebars-light) und als .docx-Bytes (base64) zurueckgegeben.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import text
|
|
from sqlalchemy.orm import Session
|
|
|
|
from classroom_engine.database import get_db
|
|
from compliance.services.founding_wizard import (
|
|
base_context,
|
|
markdown_to_docx_bytes,
|
|
render_template,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/v1/founding-wizard", tags=["founding-wizard"])
|
|
|
|
DOC_TITLES = {
|
|
"articles_of_association": "Satzung",
|
|
"gesellschafterliste": "Gesellschafterliste",
|
|
"gf_bestellungsbeschluss": "Bestellungsbeschluss Geschäftsführer",
|
|
"hrb_anmeldung": "Handelsregister-Anmeldung",
|
|
"sha": "Shareholders' Agreement (SHA)",
|
|
"geschaeftsordnung_gf": "Geschäftsordnung der Geschäftsführung",
|
|
"managing_director_employment_contract": "Geschäftsführerdienstvertrag",
|
|
"ip_assignment_agreement": "IP-Assignment Agreement",
|
|
"employment_contract_de": "Arbeitsvertrag",
|
|
"term_sheet": "Term Sheet",
|
|
"convertible_loan_agreement": "Wandeldarlehensvertrag",
|
|
"subscription_agreement": "Beteiligungsvertrag",
|
|
"esop_plan": "ESOP/VSOP-Plan",
|
|
"cap_table": "Cap Table",
|
|
}
|
|
|
|
|
|
class GenerationRequest(BaseModel):
|
|
current_step: int = 8
|
|
lifecycle_stage: str = "founding"
|
|
is_pre_notary: bool = True
|
|
basics: dict[str, Any] = {}
|
|
gesellschafter: list[dict[str, Any]] = []
|
|
capital: dict[str, Any] = {}
|
|
notar: dict[str, Any] = {}
|
|
sha: dict[str, Any] = {}
|
|
gf_contracts: list[dict[str, Any]] = []
|
|
selected_documents: list[str] = []
|
|
|
|
|
|
class DocumentResult(BaseModel):
|
|
document_type: str
|
|
title: str
|
|
filename: str
|
|
content_base64: str
|
|
size_bytes: int
|
|
generated_at: str
|
|
placeholders_count: int
|
|
|
|
|
|
class GenerationResponse(BaseModel):
|
|
documents: list[DocumentResult]
|
|
warnings: list[str] = []
|
|
|
|
|
|
def _load_template(db: Session, document_type: str) -> dict[str, Any] | None:
|
|
"""Laedt das neueste published Template fuer den document_type."""
|
|
row = db.execute(
|
|
text("""
|
|
SELECT id, document_type, title, content, placeholders, version, status
|
|
FROM compliance_legal_templates
|
|
WHERE document_type = :dt AND status = 'published'
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
"""),
|
|
{"dt": document_type},
|
|
).first()
|
|
if not row:
|
|
return None
|
|
return {
|
|
"id": str(row.id),
|
|
"document_type": row.document_type,
|
|
"title": row.title,
|
|
"content": row.content,
|
|
"placeholders": row.placeholders or [],
|
|
"version": row.version,
|
|
}
|
|
|
|
|
|
def _render_one(db: Session, doc_type: str, context: dict[str, Any]) -> DocumentResult | None:
|
|
template = _load_template(db, doc_type)
|
|
if not template:
|
|
logger.warning("No template found for document_type=%s", doc_type)
|
|
return None
|
|
rendered_md = render_template(template["content"], context)
|
|
title = template.get("title") or DOC_TITLES.get(doc_type, doc_type)
|
|
docx_bytes = markdown_to_docx_bytes(rendered_md, title=None)
|
|
from datetime import datetime
|
|
return DocumentResult(
|
|
document_type=doc_type,
|
|
title=title,
|
|
filename=f"{doc_type}_{context.get('COMPANY_NAME', 'Unternehmen')}.docx".replace(" ", "_"),
|
|
content_base64=base64.b64encode(docx_bytes).decode("ascii"),
|
|
size_bytes=len(docx_bytes),
|
|
generated_at=datetime.utcnow().isoformat() + "Z",
|
|
placeholders_count=len(template.get("placeholders") or []),
|
|
)
|
|
|
|
|
|
@router.post("/generate", response_model=GenerationResponse)
|
|
def generate_documents(req: GenerationRequest, request: Request) -> GenerationResponse:
|
|
"""Hauptendpunkt: nimmt Wizard-State entgegen, generiert DOCX fuer alle ausgewaehlten Dokumente."""
|
|
# Database session is provided via FastAPI dependency injection in production.
|
|
# Hier vereinfacht direkt aus dem request state (verwendet Hauptverbindung)
|
|
from classroom_engine.database import SessionLocal
|
|
db: Session = SessionLocal()
|
|
try:
|
|
context = base_context(req.model_dump())
|
|
results: list[DocumentResult] = []
|
|
warnings: list[str] = []
|
|
|
|
for doc_type in req.selected_documents:
|
|
result = _render_one(db, doc_type, context)
|
|
if result is None:
|
|
warnings.append(f"Template '{doc_type}' nicht in Datenbank gefunden")
|
|
continue
|
|
results.append(result)
|
|
|
|
if not results:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Keines der angeforderten Dokumente konnte generiert werden. "
|
|
f"Warnings: {warnings}"
|
|
)
|
|
|
|
return GenerationResponse(documents=results, warnings=warnings)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.get("/templates")
|
|
def list_available_templates(request: Request) -> dict[str, Any]:
|
|
"""Listet alle verfuegbaren Templates mit Kategorisierung."""
|
|
from classroom_engine.database import SessionLocal
|
|
db: Session = SessionLocal()
|
|
try:
|
|
rows = db.execute(
|
|
text("""
|
|
SELECT document_type, title, description, version, status,
|
|
lifecycle_stage, functional_category
|
|
FROM compliance_legal_templates
|
|
WHERE status = 'published'
|
|
ORDER BY functional_category, document_type
|
|
""")
|
|
).fetchall()
|
|
return {
|
|
"templates": [
|
|
{
|
|
"document_type": r.document_type,
|
|
"title": r.title,
|
|
"description": r.description,
|
|
"version": r.version,
|
|
"lifecycle_stage": list(r.lifecycle_stage or []),
|
|
"functional_category": r.functional_category,
|
|
}
|
|
for r in rows
|
|
],
|
|
"count": len(rows),
|
|
}
|
|
finally:
|
|
db.close()
|