diff --git a/SANIERUNGS_FORTSCHRITT.md b/SANIERUNGS_FORTSCHRITT.md index 66353bf..0a5c7b9 100644 --- a/SANIERUNGS_FORTSCHRITT.md +++ b/SANIERUNGS_FORTSCHRITT.md @@ -1,3 +1,4 @@ +ÜBERHOLT – NICHT ALS UMSETZUNGSANWEISUNG VERWENDEN # LeoCRM Sanierungsfortschritt **Letztes Update:** 2026-08-03 diff --git a/app/commands/create_contact.py b/app/commands/create_contact.py deleted file mode 100644 index d5d8876..0000000 --- a/app/commands/create_contact.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Example command: CreateContact using the Command Pattern. - -This is a reference implementation for new modules. -Existing contact_service.py is NOT changed — this is an alternative path. - -Usage: - @router.post("/contacts-v2") - async def create_contact_v2( - body: CreateContactDTO, - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(require_permission("contacts:write")), - ): - ctx = RequestContext( - user_id=uuid.UUID(current_user["user_id"]), - tenant_id=uuid.UUID(current_user["tenant_id"]), - is_system_admin=current_user.get("is_system_admin", False), - permissions=set(current_user.get("permissions", [])), - ) - cmd = CreateContactCommand( - firstname=body.firstname, - surname=body.surname, - email=body.email, - ) - handler = CreateContactHandler() - return await handler.execute(cmd, ctx, db) -""" - -from __future__ import annotations - -import uuid -from dataclasses import dataclass -from typing import Any - -from app.core.commands import CommandHandler, RequestContext, UnitOfWork -from app.models.contact import Contact - - -@dataclass -class CreateContactCommand: - """Command to create a new contact.""" - firstname: str - surname: str - email: str | None = None - phone: str | None = None - company: str | None = None - - -class CreateContactHandler(CommandHandler[CreateContactCommand, dict[str, Any]]): - """Handler for CreateContactCommand. - - Demonstrates the Command Pattern: - 1. Authorization check (ctx.require) - 2. Domain operation (create Contact) - 3. Outbox event (crm.contact.created.v1) - 4. Audit log (contact.created) - 5. Single commit via UoW - """ - - async def handle(self, cmd: CreateContactCommand, ctx: RequestContext, uow: UnitOfWork) -> dict[str, Any]: - # 1. Authorization - ctx.require("contacts:write") - - # 2. Domain operation - contact = Contact( - tenant_id=ctx.tenant_id, - firstname=cmd.firstname, - surname=cmd.surname, - email_1=cmd.email, - phone_1=cmd.phone, - company=cmd.company, - owner_id=ctx.user_id, - created_by=ctx.user_id, - updated_by=ctx.user_id, - ) - uow.add(contact) - - # 3. Outbox event (standardized envelope) - uow.outbox_add( - event_name="crm.contact.created.v1", - aggregate_id=contact.id, # Will be set after flush - aggregate_type="contact", - payload={ - "firstname": cmd.firstname, - "surname": cmd.surname, - "email": cmd.email, - }, - ) - - # 4. Audit log - uow.audit_record( - action="create", - entity_id=contact.id, - entity_type="contact", - changes={"firstname": cmd.firstname, "surname": cmd.surname, "email": cmd.email}, - ) - - # 5. Return dict (will be populated after flush in commit) - return { - "id": str(contact.id), - "firstname": contact.firstname, - "surname": contact.surname, - "email_1": contact.email_1, - } diff --git a/app/core/commands.py b/app/core/commands.py deleted file mode 100644 index 27f5dc6..0000000 --- a/app/core/commands.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Command pattern infrastructure for new modules. - -This provides a clean, transactional command handler pattern: - - HTTP Route → Command Handler → Authorization → Domain Operation → Audit + Outbox → one Commit - -Existing services are NOT refactored — they continue to work as-is. -New modules (ERP, etc.) should use this pattern. - -Usage: - - @dataclass - class CreateInvoiceCommand: - customer_id: uuid.UUID - amount: Decimal - - class CreateInvoiceHandler(CommandHandler[CreateInvoiceCommand, Invoice]): - async def handle(self, cmd: CreateInvoiceCommand, ctx: RequestContext, uow: UnitOfWork) -> Invoice: - ctx.require("invoices:create") - invoice = Invoice.create(tenant_id=ctx.tenant_id, owner_id=ctx.user_id, ...) - uow.add(invoice) - uow.outbox.add("crm.invoice.created.v1", invoice.id, "invoice", invoice.to_dict()) - uow.audit.record("invoice.created", invoice.id) - return invoice - - # In route: - @router.post("/invoices") - async def create_invoice(body: CreateInvoiceDTO, ctx: RequestContext = Depends(get_request_context)): - cmd = CreateInvoiceCommand(customer_id=body.customer_id, amount=body.amount) - handler = CreateInvoiceHandler() - result = await handler.execute(cmd, ctx) - return result -""" - -from __future__ import annotations - -import uuid -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any, Generic, TypeVar - -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.audit import log_audit -from app.core.outbox import enqueue_outbox_event - - -TCommand = TypeVar("TCommand") -TResult = TypeVar("TResult") - - -@dataclass -class RequestContext: - """Request context with user, tenant, and permission info. - - Passed to every command handler. Provides authorization checks. - """ - user_id: uuid.UUID - tenant_id: uuid.UUID - is_system_admin: bool = False - permissions: set[str] = field(default_factory=set) - correlation_id: uuid.UUID = field(default_factory=uuid.uuid4) - - def require(self, permission: str) -> None: - """Require a permission. Raises PermissionError if not granted.""" - if self.is_system_admin: - return - if permission not in self.permissions: - raise PermissionError(f"Missing permission: {permission}") - - def has(self, permission: str) -> bool: - """Check if user has a permission.""" - if self.is_system_admin: - return True - return permission in self.permissions - - -class UnitOfWork: - """Unit of Work — collects changes, audit, and outbox events. - - One UoW per business operation. Commit happens once at the end. - """ - - def __init__(self, db: AsyncSession, tenant_id: uuid.UUID, user_id: uuid.UUID): - self.db = db - self.tenant_id = tenant_id - self.user_id = user_id - self._audit_entries: list[dict[str, Any]] = [] - self._outbox_events: list[dict[str, Any]] = [] - - def add(self, entity: Any) -> None: - """Add an entity to the session.""" - self.db.add(entity) - - def outbox_add( - self, - event_name: str, - aggregate_id: uuid.UUID, - aggregate_type: str, - payload: dict[str, Any], - schema_version: int = 1, - ) -> None: - """Queue an outbox event for commit.""" - self._outbox_events.append({ - "event_name": event_name, - "aggregate_id": aggregate_id, - "aggregate_type": aggregate_type, - "payload": payload, - "schema_version": schema_version, - }) - - def audit_record(self, action: str, entity_id: uuid.UUID, entity_type: str = "", changes: dict[str, Any] | None = None) -> None: - """Queue an audit log entry for commit.""" - self._audit_entries.append({ - "action": action, - "entity_id": entity_id, - "entity_type": entity_type, - "changes": changes or {}, - }) - - async def commit(self) -> None: - """Flush, write audit + outbox, then commit.""" - # Flush to get entity IDs - await self.db.flush() - - # Write outbox events - for evt in self._outbox_events: - await enqueue_outbox_event( - self.db, - self.tenant_id, - evt["event_name"], - evt["payload"], - aggregate_type=evt["aggregate_type"], - aggregate_id=evt["aggregate_id"], - schema_version=evt["schema_version"], - ) - - # Write audit entries - for entry in self._audit_entries: - await log_audit( - self.db, - self.tenant_id, - self.user_id, - entry["action"], - entry["entity_type"], - entry["entity_id"], - changes=entry["changes"], - ) - - # Single commit for everything - await self.db.commit() - - async def rollback(self) -> None: - """Rollback the transaction.""" - await self.db.rollback() - - -class CommandHandler(ABC, Generic[TCommand, TResult]): - """Base class for command handlers. - - Subclasses implement `handle()` with the business logic. - The `execute()` method wraps it with UoW creation and error handling. - """ - - @abstractmethod - async def handle(self, command: TCommand, ctx: RequestContext, uow: UnitOfWork) -> TResult: - """Business logic. Use uow.add(), uow.outbox_add(), uow.audit_record().""" - ... - - async def execute(self, command: TCommand, ctx: RequestContext, db: AsyncSession) -> TResult: - """Execute the command with a Unit of Work. - - Creates a UoW, calls handle(), commits on success, rolls back on error. - """ - uow = UnitOfWork(db, ctx.tenant_id, ctx.user_id) - try: - result = await self.handle(command, ctx, uow) - await uow.commit() - return result - except Exception: - await uow.rollback() - raise - - -# ── FastAPI Dependency ─────────────────────────────────────────────────────── - -async def get_request_context( - current_user: dict = None, # Will be injected by FastAPI with require_permission -) -> RequestContext: - """Build a RequestContext from the current user. - - Usage in routes: - ctx: RequestContext = Depends(get_request_context) - """ - if current_user is None: - raise PermissionError("Not authenticated") - return RequestContext( - user_id=uuid.UUID(current_user["user_id"]), - tenant_id=uuid.UUID(current_user["tenant_id"]), - is_system_admin=current_user.get("is_system_admin", False), - permissions=set(current_user.get("permissions", [])), - ) diff --git a/docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md b/docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md index 93fc0ce..035c9e4 100644 --- a/docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md +++ b/docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md @@ -1,3 +1,4 @@ +ÜBERHOLT – NICHT ALS UMSETZUNGSANWEISUNG VERWENDEN # LeoCRM — Abschlussbericht Phase 0 + Phase 1 und vollständiger Sanierungsplan **Datum:** 2026-08-01 diff --git a/docs/RECOVERY_SCOPE.md b/docs/RECOVERY_SCOPE.md new file mode 100644 index 0000000..84e0858 --- /dev/null +++ b/docs/RECOVERY_SCOPE.md @@ -0,0 +1,65 @@ +# LeoCRM Recovery Scope + +**Erstellt:** 2026-08-03 +**Git-Tag:** `pre-recovery-current` (3cbf921) +**Branch:** `recovery/minimal-finish` +**Alembic-Head:** 0092 + +> Diese Datei ist die einzige verbindliche Quelle für den Reparatur- und Abschlussplan. +> Alle früheren Umbau- und Abschlussdokumente sind überholt. + +--- + +## Verbindliche Regeln + +1. Keine neue Zielarchitektur entwerfen. +2. Keine Microservices einführen. +3. Keine neuen generischen Security-, Entity-, Storage- oder Agentenplattformen bauen. +4. Bestehende Services nicht vollständig auf Commands umbauen. +5. Keine Beispiele als Produktanforderungen behandeln. +6. Keine Migration bis einschließlich `0092` erneut verändern. +7. Schemafehler ausschließlich über neue Forward-Migrationen korrigieren. +8. Keine produktiven Daten automatisch zusammenführen oder löschen. +9. Keine manuellen Änderungen in laufenden Coolify-Containern. +10. Jeder Arbeitsschritt benötigt: konkreten Fehler, begrenzte Codeänderung, reproduzierbaren Test, eigenen Git-Commit. + +--- + +## Phasen-Status + +| Phase | Status | Hinweis | +|-------|--------|---------| +| 0 — Stand sichern | ✅ Abgeschlossen | Tag + Branch + RECOVERY_SCOPE.md | +| 1 — Migrationen & Zielschema | ⏳ Nicht begonnen | Migrationsaudit + Forward-Migrationen ab 0093 | +| 2 — Security & Permissions | 🔶 Teilweise erledigt | Workspace-Permissions registriert, Frontend-Fallback entfernt. RLS-Tests offen. | +| 3 — Doppelte Command-Struktur | ⏳ Nicht begonnen | app/core/commands.py + app/commands/create_contact.py entfernen | +| 4 — Workspaces | 🔶 Teilweise erledigt | Widget-Fixes, Permissions, Context is_visible, AVAILABLE_MODULES entfernt. Kalenderauswahl, Modulunterpunkte offen. | +| 5 — AI & MCP | ⏳ Nicht begonnen | Delegationstoken, Bearer-Auth, Pfadbegrenzung | +| 6 — DMS & Attachments | ⏳ Nicht begonnen | Streaming, Deduplikation, Alt-Migration | +| 7 — Plugins, Worker, Outbox | ⏳ Nicht begonnen | Plugin-Gate, Event-Envelope, Handler-Tracking | +| 8 — CI, Restore, Coolify | ⏳ Nicht begonnen | Merge-CI, Migrations-Gate, Restore-Test | +| 9 — Abschluss | ⏳ Nicht begonnen | RECOVERY_ACCEPTANCE_REPORT.md | + +--- + +## Produktionsstand (Phase 0.1) + +- **Git-Commit:** 3cbf921 (main) +- **Alembic-Version:** 0092 +- **Produktions-URL:** https://crm.media-on.de — healthy +- **API:** healthy, Worker: healthy +- **RLS-Tabellen:** 109 +- **Attachments (alt):** 0 +- **Entity-Attachments:** 2 +- **DMS-Dateien:** 17 +- **Workspaces:** 2 + +--- + +## Überholte Dokumente + +Folgende Dokumente sind nicht mehr als Umsetzungsanweisung zu verwenden: + +- `docs/ABSCHLUSSBERICHT_PHASE0_PHASE1.md` — ÜBERHOLT +- `SANIERUNGS_FORTSCHRITT.md` — ÜBERHOLT +- `docs/phase0_phase1_acceptance_report.md` — ÜBERHOLT diff --git a/docs/phase0_phase1_acceptance_report.md b/docs/phase0_phase1_acceptance_report.md index 34e608a..c65f306 100644 --- a/docs/phase0_phase1_acceptance_report.md +++ b/docs/phase0_phase1_acceptance_report.md @@ -1,3 +1,4 @@ +ÜBERHOLT – NICHT ALS UMSETZUNGSANWEISUNG VERWENDEN # Phase 0 + Phase 1 — Abschluss-Abnahmeprotokoll **Stand:** 2026-07-31 12:06 CEST