""" Audit Log Events """ import logging from sqlalchemy import event, inspect from sqlalchemy.orm import Session from api.v1.modules.a76.general_catalogs.company.models import Company from api.v1.modules.a76.invoices.models import InvoiceHeader from core.database import rls_company_var, rls_tenant_var from .services.service import AuditService from .utils.serialization import serialize_for_json from core.context import get_user_context logger = logging.getLogger(__name__) def register_audit_listeners(models_to_audit): """ Register SQLAlchemy listeners for given models """ for model in models_to_audit: event.listen(model, "after_insert", after_insert_listener) event.listen(model, "after_update", after_update_listener) event.listen(model, "after_delete", after_delete_listener) def _get_current_username(): try: context = get_user_context() if context: # Token usually has 'preferred_username' or 'name' or 'sub' return ( context.get("preferred_username") or context.get("email") or context.get("sub") or "System" ) except Exception: pass return "System" def _resolve_audit_company_tenant(session: Session, target) -> tuple: """ company_id / tenant_id desde la fila ORM, ContextVars RLS (petición HTTP), o ``Company.tenant_id`` por ``company_id``. """ resolution_source = "target" company_id = getattr(target, "company_id", None) if company_id is None and getattr(target, "__tablename__", None) == "company": company_id = getattr(target, "id", None) if company_id is not None: resolution_source = "company_self_id" if company_id is None: company_id = rls_company_var.get() if company_id is not None: resolution_source = "rls_context" tenant_id = getattr(target, "tenant_id", None) if tenant_id is None: tenant_id = rls_tenant_var.get() if tenant_id is not None and resolution_source == "target": resolution_source = "rls_context" # Common fallback for invoice child tables where invoice_id points to header scope. if (company_id is None or tenant_id is None) and hasattr(target, "invoice_id"): invoice_id = getattr(target, "invoice_id", None) if invoice_id is not None: invoice_scope = ( session.query(InvoiceHeader.company_id, InvoiceHeader.tenant_id) .filter(InvoiceHeader.id == invoice_id, InvoiceHeader.deleted_at.is_(None)) .first() ) if invoice_scope: if company_id is None: company_id = invoice_scope[0] if tenant_id is None: tenant_id = invoice_scope[1] resolution_source = "invoice_header_lookup" if tenant_id is None and company_id is not None: row = ( session.query(Company.tenant_id) .filter(Company.id == company_id, Company.deleted_at.is_(None)) .first() ) if row: tenant_id = int(row[0]) resolution_source = "company_lookup" return company_id, tenant_id, resolution_source def after_insert_listener(mapper, connection, target): """ Listener for INSERT operations """ table_name = target.__tablename__ record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() session = Session(bind=connection) try: company_id, tenant_id, resolution_source = _resolve_audit_company_tenant( session, target ) if company_id is None or tenant_id is None: logger.warning( "Audit skip INSERT table=%s record_id=%s company_id=%s tenant_id=%s source=%s", table_name, getattr(target, "id", None), company_id, tenant_id, resolution_source, ) return AuditService.log_crud_operation( db=session, table_name=table_name, operation_type="CREATE", record_data=record_data, username=username, record_id=str(getattr(target, "id", "")), company_id=company_id, tenant_id=tenant_id, ) except Exception as e: logger.warning("Error logging insert for %s: %s", table_name, e) finally: session.close() def after_update_listener(mapper, connection, target): """ Listener for UPDATE operations """ table_name = target.__tablename__ state = inspect(target) changes = {} old_values = {} new_values = {} for attr in state.attrs: hist = attr.history if hist.has_changes(): changes[attr.key] = hist.added[0] if hist.added else None old_values[attr.key] = hist.deleted[0] if hist.deleted else None new_values[attr.key] = hist.added[0] if hist.added else None if not changes: return record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() session = Session(bind=connection) try: company_id, tenant_id, resolution_source = _resolve_audit_company_tenant( session, target ) if company_id is None or tenant_id is None: logger.warning( "Audit skip UPDATE table=%s record_id=%s company_id=%s tenant_id=%s source=%s", table_name, getattr(target, "id", None), company_id, tenant_id, resolution_source, ) return AuditService.log_crud_operation( db=session, table_name=table_name, operation_type="UPDATE", record_data=record_data, username=username, record_id=str(getattr(target, "id", "")), old_values=serialize_for_json(old_values), new_values=serialize_for_json(new_values), company_id=company_id, tenant_id=tenant_id, ) except Exception as e: logger.warning("Error logging update for %s: %s", table_name, e) finally: session.close() def after_delete_listener(mapper, connection, target): """ Listener for DELETE operations """ table_name = target.__tablename__ record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() session = Session(bind=connection) try: company_id, tenant_id, resolution_source = _resolve_audit_company_tenant( session, target ) if company_id is None or tenant_id is None: logger.warning( "Audit skip DELETE table=%s record_id=%s company_id=%s tenant_id=%s source=%s", table_name, getattr(target, "id", None), company_id, tenant_id, resolution_source, ) return AuditService.log_crud_operation( db=session, table_name=table_name, operation_type="DELETE", record_data=record_data, username=username, record_id=str(getattr(target, "id", "")), company_id=company_id, tenant_id=tenant_id, ) except Exception as e: logger.warning("Error logging delete for %s: %s", table_name, e) finally: session.close()