""" Audit Logging Service for Time Tracking Module =============================================== Fuld sporbarhed af alle handlinger i modulet. Alle events logges til tmodule_sync_log for compliance. """ import logging import json from datetime import datetime from typing import Optional, Dict, Any from app.core.database import execute_insert from app.timetracking.backend.models import TModuleSyncLogCreate logger = logging.getLogger(__name__) class AuditLogger: """ Service til at logge alle module events. Event Types: - sync_started, sync_completed, sync_failed - approval, rejection, bulk_approval - order_created, order_updated, order_cancelled - export_started, export_completed, export_failed - module_installed, module_uninstalled """ @staticmethod def log_event( event_type: str, entity_type: Optional[str] = None, entity_id: Optional[int] = None, user_id: Optional[int] = None, details: Optional[Dict[str, Any]] = None, ip_address: Optional[str] = None, user_agent: Optional[str] = None ) -> int: """ Log en event til tmodule_sync_log. Args: event_type: Type af event (sync_started, approval, etc.) entity_type: Type af entitet (time_entry, order, customer, case) entity_id: ID på entiteten user_id: ID på brugeren der foretog handlingen details: Ekstra detaljer som JSON ip_address: IP-adresse user_agent: User agent string Returns: ID på den oprettede log-entry """ try: # Konverter details til JSON-string hvis det er en dict details_json = json.dumps(details) if details else None query = """ INSERT INTO tmodule_sync_log (event_type, entity_type, entity_id, user_id, details, ip_address, user_agent) VALUES (%s, %s, %s, %s, %s::jsonb, %s, %s) RETURNING id """ log_id = execute_insert( query, (event_type, entity_type, entity_id, user_id, details_json, ip_address, user_agent) ) logger.debug(f"📝 Logged event: {event_type} (ID: {log_id})") return log_id except Exception as e: logger.error(f"❌ Failed to log event {event_type}: {e}") # Don't raise - audit logging failure shouldn't break main flow return 0 @staticmethod def log_sync_started(user_id: Optional[int] = None) -> int: """Log start af vTiger sync""" return AuditLogger.log_event( event_type="sync_started", user_id=user_id, details={"timestamp": datetime.now().isoformat()} ) @staticmethod def log_sync_completed( stats: Dict[str, Any], user_id: Optional[int] = None ) -> int: """Log succesfuld sync med statistik""" return AuditLogger.log_event( event_type="sync_completed", user_id=user_id, details=stats ) @staticmethod def log_sync_failed( error: str, user_id: Optional[int] = None ) -> int: """Log fejlet sync""" return AuditLogger.log_event( event_type="sync_failed", user_id=user_id, details={"error": error, "timestamp": datetime.now().isoformat()} ) @staticmethod def log_approval( time_id: int, original_hours: float, approved_hours: float, rounded_to: Optional[float], note: Optional[str], user_id: Optional[int] = None ) -> int: """Log godkendelse af tidsregistrering""" return AuditLogger.log_event( event_type="approval", entity_type="time_entry", entity_id=time_id, user_id=user_id, details={ "original_hours": original_hours, "approved_hours": approved_hours, "rounded_to": rounded_to, "note": note, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_rejection( time_id: int, reason: Optional[str], user_id: Optional[int] = None ) -> int: """Log afvisning af tidsregistrering""" return AuditLogger.log_event( event_type="rejection", entity_type="time_entry", entity_id=time_id, user_id=user_id, details={ "reason": reason, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_order_created( order_id: int, customer_id: int, total_hours: float, total_amount: float, line_count: int, user_id: Optional[int] = None ) -> int: """Log oprettelse af ordre""" return AuditLogger.log_event( event_type="order_created", entity_type="order", entity_id=order_id, user_id=user_id, details={ "customer_id": customer_id, "total_hours": total_hours, "total_amount": total_amount, "line_count": line_count, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_order_updated( order_id: int, changes: Dict[str, Any], user_id: Optional[int] = None ) -> int: """Log opdatering af ordre""" return AuditLogger.log_event( event_type="order_updated", entity_type="order", entity_id=order_id, user_id=user_id, details={ "changes": changes, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_order_cancelled( order_id: int, reason: Optional[str], user_id: Optional[int] = None ) -> int: """Log annullering af ordre""" return AuditLogger.log_event( event_type="order_cancelled", entity_type="order", entity_id=order_id, user_id=user_id, details={ "reason": reason, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_export_started( order_id: int, user_id: Optional[int] = None ) -> int: """Log start af e-conomic export""" return AuditLogger.log_event( event_type="export_started", entity_type="order", entity_id=order_id, user_id=user_id, details={"timestamp": datetime.now().isoformat()} ) @staticmethod def log_export_completed( order_id: int, economic_draft_id: Optional[int], economic_order_number: Optional[str], dry_run: bool, user_id: Optional[int] = None ) -> int: """Log succesfuld export til e-conomic""" return AuditLogger.log_event( event_type="export_completed", entity_type="order", entity_id=order_id, user_id=user_id, details={ "economic_draft_id": economic_draft_id, "economic_order_number": economic_order_number, "dry_run": dry_run, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_export_failed( order_id: int, error: str, user_id: Optional[int] = None ) -> int: """Log fejlet export til e-conomic""" return AuditLogger.log_event( event_type="export_failed", entity_type="order", entity_id=order_id, user_id=user_id, details={ "error": error, "timestamp": datetime.now().isoformat() } ) @staticmethod def log_module_uninstalled(user_id: Optional[int] = None) -> int: """Log modul-uninstall""" return AuditLogger.log_event( event_type="module_uninstalled", user_id=user_id, details={"timestamp": datetime.now().isoformat()} ) # Singleton instance audit = AuditLogger()