"""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", [])), )