bmc_hub/app/timetracking/backend/audit.py

281 lines
8.4 KiB
Python
Raw Normal View History

"""
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 entiteten
user_id: ID brugeren der foretog handlingen
details: Ekstra detaljer som JSON
ip_address: IP-adresse
user_agent: User agent string
Returns:
ID 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()