""" Zentrale Notification-Utilities für manuelle Eingriffe ======================================================= Wenn Advoware-API-Limitierungen existieren (z.B. READ-ONLY Felder), werden Notifications in EspoCRM erstellt, damit User manuelle Eingriffe vornehmen können. Features: - Notifications an assigned Users - Task-Erstellung für manuelle Eingriffe - Zentrale Verwaltung aller Notification-Types """ from typing import Dict, Any, Optional, Literal, List from datetime import datetime, timedelta import logging class NotificationManager: """ Zentrale Klasse für Notifications bei Sync-Problemen """ def __init__(self, espocrm_api, context=None): """ Args: espocrm_api: EspoCRMAPI instance context: Optional context für Logging """ self.espocrm = espocrm_api self.context = context self.logger = context.logger if context else logging.getLogger(__name__) async def notify_manual_action_required( self, entity_type: str, entity_id: str, action_type: Literal[ "address_delete_required", "address_reactivate_required", "address_field_update_required", "readonly_field_conflict", "missing_in_advoware", "general_manual_action" ], details: Dict[str, Any], assigned_user_id: Optional[str] = None, create_task: bool = True ) -> Dict[str, str]: """ Erstellt Notification und optional Task für manuelle Eingriffe Args: entity_type: EspoCRM Entity Type (z.B. 'CAdressen', 'CBeteiligte') entity_id: Entity ID in EspoCRM action_type: Art der manuellen Aktion details: Detaillierte Informationen assigned_user_id: User der benachrichtigt werden soll (optional) create_task: Ob zusätzlich ein Task erstellt werden soll Returns: Dict mit notification_id und optional task_id """ try: # Hole Entity-Daten entity = await self.espocrm.get_entity(entity_type, entity_id) entity_name = entity.get('name', f"{entity_type} {entity_id}") # Falls kein assigned_user, versuche aus Entity zu holen if not assigned_user_id: assigned_user_id = entity.get('assignedUserId') # Erstelle Notification notification_data = self._build_notification_message( action_type, entity_type, entity_name, details ) notification_id = await self._create_notification( user_id=assigned_user_id, message=notification_data['message'], entity_type=entity_type, entity_id=entity_id ) result = {'notification_id': notification_id} # Optional: Task erstellen if create_task: task_id = await self._create_task( name=notification_data['task_name'], description=notification_data['task_description'], parent_type=entity_type, parent_id=entity_id, assigned_user_id=assigned_user_id, priority=notification_data['priority'] ) result['task_id'] = task_id self.logger.info( f"Manual action notification created: {action_type} for " f"{entity_type}/{entity_id}" ) return result except Exception as e: self.logger.error(f"Failed to create notification: {e}") raise def _build_notification_message( self, action_type: str, entity_type: str, entity_name: str, details: Dict[str, Any] ) -> Dict[str, str]: """ Erstellt Notification-Message basierend auf Action-Type Returns: Dict mit 'message', 'task_name', 'task_description', 'priority' """ if action_type == "address_delete_required": return { 'message': ( f"🗑️ Adresse in Advoware löschen erforderlich\n" f"Adresse: {entity_name}\n" f"Grund: Advoware API unterstützt kein DELETE und gueltigBis ist READ-ONLY\n" f"Bitte manuell in Advoware löschen oder deaktivieren." ), 'task_name': f"Adresse in Advoware löschen: {entity_name}", 'task_description': ( f"MANUELLE AKTION ERFORDERLICH\n\n" f"Adresse: {entity_name}\n" f"BetNr: {details.get('betnr', 'N/A')}\n" f"Adresse: {details.get('strasse', '')}, {details.get('plz', '')} {details.get('ort', '')}\n\n" f"GRUND:\n" f"- DELETE API nicht verfügbar (403 Forbidden)\n" f"- gueltigBis ist READ-ONLY (kann nicht nachträglich gesetzt werden)\n\n" f"AKTION:\n" f"1. In Advoware Web-Interface einloggen\n" f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n" f"3. Adresse suchen: {details.get('strasse', '')}\n" f"4. Adresse löschen oder deaktivieren\n\n" f"Nach Erledigung: Task als 'Completed' markieren." ), 'priority': 'Normal' } elif action_type == "address_reactivate_required": return { 'message': ( f"♻️ Adresse-Reaktivierung in Advoware erforderlich\n" f"Adresse: {entity_name}\n" f"Grund: gueltigBis kann nicht nachträglich geändert werden\n" f"Bitte neue Adresse in Advoware erstellen." ), 'task_name': f"Neue Adresse in Advoware erstellen: {entity_name}", 'task_description': ( f"MANUELLE AKTION ERFORDERLICH\n\n" f"Adresse: {entity_name}\n" f"BetNr: {details.get('betnr', 'N/A')}\n\n" f"GRUND:\n" f"Diese Adresse wurde reaktiviert, aber die alte Adresse in Advoware " f"ist abgelaufen (gueltigBis in Vergangenheit). Da gueltigBis READ-ONLY ist, " f"muss eine neue Adresse erstellt werden.\n\n" f"AKTION:\n" f"1. In Advoware Web-Interface einloggen\n" f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n" f"3. Neue Adresse erstellen:\n" f" - Straße: {details.get('strasse', '')}\n" f" - PLZ: {details.get('plz', '')}\n" f" - Ort: {details.get('ort', '')}\n" f" - Land: {details.get('land', '')}\n" f" - Bemerkung: EspoCRM-ID: {details.get('espocrm_id', '')}\n" f"4. Sync erneut durchführen, damit Mapping aktualisiert wird\n\n" f"Nach Erledigung: Task als 'Completed' markieren." ), 'priority': 'Normal' } elif action_type == "address_field_update_required": readonly_fields = details.get('readonly_fields', []) return { 'message': ( f"⚠️ Adressfelder in Advoware können nicht aktualisiert werden\n" f"Adresse: {entity_name}\n" f"READ-ONLY Felder: {', '.join(readonly_fields)}\n" f"Bitte manuell in Advoware ändern." ), 'task_name': f"Adressfelder in Advoware aktualisieren: {entity_name}", 'task_description': ( f"MANUELLE AKTION ERFORDERLICH\n\n" f"Adresse: {entity_name}\n" f"BetNr: {details.get('betnr', 'N/A')}\n\n" f"GRUND:\n" f"Folgende Felder sind in Advoware API READ-ONLY und können nicht " f"via PUT geändert werden:\n" f"- {', '.join(readonly_fields)}\n\n" f"GEWÜNSCHTE ÄNDERUNGEN:\n" + '\n'.join([f" - {k}: {v}" for k, v in details.get('changes', {}).items()]) + f"\n\nAKTION:\n" f"1. In Advoware Web-Interface einloggen\n" f"2. Beteiligten mit BetNr {details.get('betnr', 'N/A')} öffnen\n" f"3. Adresse suchen und obige Felder manuell ändern\n" f"4. Sync erneut durchführen zur Bestätigung\n\n" f"Nach Erledigung: Task als 'Completed' markieren." ), 'priority': 'Low' } elif action_type == "readonly_field_conflict": return { 'message': ( f"⚠️ Sync-Konflikt bei READ-ONLY Feldern\n" f"{entity_type}: {entity_name}\n" f"Änderungen konnten nicht synchronisiert werden." ), 'task_name': f"Sync-Konflikt prüfen: {entity_name}", 'task_description': ( f"SYNC-KONFLIKT\n\n" f"{entity_type}: {entity_name}\n\n" f"PROBLEM:\n" f"Felder wurden in EspoCRM geändert, sind aber in Advoware READ-ONLY.\n\n" f"BETROFFENE FELDER:\n" + '\n'.join([f" - {k}: {v}" for k, v in details.get('conflicts', {}).items()]) + f"\n\nOPTIONEN:\n" f"1. Änderungen in EspoCRM rückgängig machen (Advoware = Master)\n" f"2. Änderungen manuell in Advoware vornehmen\n" f"3. Feld als 'nicht synchronisiert' akzeptieren\n\n" f"Nach Entscheidung: Task als 'Completed' markieren." ), 'priority': 'Normal' } elif action_type == "missing_in_advoware": return { 'message': ( f"❓ Element fehlt in Advoware\n" f"{entity_type}: {entity_name}\n" f"Bitte manuell in Advoware erstellen." ), 'task_name': f"In Advoware erstellen: {entity_name}", 'task_description': ( f"MANUELLE AKTION ERFORDERLICH\n\n" f"{entity_type}: {entity_name}\n\n" f"GRUND:\n" f"Dieses Element existiert in EspoCRM, aber nicht in Advoware.\n" f"Möglicherweise wurde es direkt in EspoCRM erstellt.\n\n" f"DATEN:\n" + '\n'.join([f" - {k}: {v}" for k, v in details.items() if k != 'espocrm_id']) + f"\n\nAKTION:\n" f"1. In Advoware Web-Interface einloggen\n" f"2. Element mit obigen Daten manuell erstellen\n" f"3. Sync erneut durchführen für Mapping\n\n" f"Nach Erledigung: Task als 'Completed' markieren." ), 'priority': 'Normal' } else: # general_manual_action return { 'message': ( f"🔧 Manuelle Aktion erforderlich\n" f"{entity_type}: {entity_name}\n" f"{details.get('message', 'Bitte prüfen.')}" ), 'task_name': f"Manuelle Aktion: {entity_name}", 'task_description': ( f"MANUELLE AKTION ERFORDERLICH\n\n" f"{entity_type}: {entity_name}\n\n" f"{details.get('description', 'Keine Details verfügbar.')}" ), 'priority': details.get('priority', 'Normal') } async def _create_notification( self, user_id: Optional[str], message: str, entity_type: str, entity_id: str ) -> str: """ Erstellt EspoCRM Notification (In-App) Returns: notification_id """ if not user_id: self.logger.warning("No user assigned - notification not created") return None notification_data = { 'type': 'Message', 'message': message, 'userId': user_id, 'relatedType': entity_type, 'relatedId': entity_id, 'read': False } try: result = await self.espocrm.create_entity('Notification', notification_data) return result.get('id') except Exception as e: self.logger.error(f"Failed to create notification: {e}") return None async def _create_task( self, name: str, description: str, parent_type: str, parent_id: str, assigned_user_id: Optional[str], priority: str = 'Normal' ) -> str: """ Erstellt EspoCRM Task Returns: task_id """ # Due Date: 7 Tage in Zukunft due_date = (datetime.now() + timedelta(days=7)).strftime('%Y-%m-%d') task_data = { 'name': name, 'description': description, 'status': 'Not Started', 'priority': priority, 'dateEnd': due_date, 'parentType': parent_type, 'parentId': parent_id, 'assignedUserId': assigned_user_id } try: result = await self.espocrm.create_entity('Task', task_data) return result.get('id') except Exception as e: self.logger.error(f"Failed to create task: {e}") return None async def resolve_task(self, task_id: str) -> bool: """ Markiert Task als erledigt Args: task_id: Task ID Returns: True wenn erfolgreich """ try: await self.espocrm.update_entity('Task', task_id, { 'status': 'Completed' }) return True except Exception as e: self.logger.error(f"Failed to complete task {task_id}: {e}") return False # Helper-Funktionen für häufige Use-Cases async def notify_address_delete_required( notification_manager: NotificationManager, address_entity_id: str, betnr: str, address_data: Dict[str, Any] ) -> Dict[str, str]: """ Shortcut: Notification für Adresse löschen """ return await notification_manager.notify_manual_action_required( entity_type='CAdressen', entity_id=address_entity_id, action_type='address_delete_required', details={ 'betnr': betnr, 'strasse': address_data.get('adresseStreet'), 'plz': address_data.get('adressePostalCode'), 'ort': address_data.get('adresseCity'), 'espocrm_id': address_entity_id } ) async def notify_address_readonly_fields( notification_manager: NotificationManager, address_entity_id: str, betnr: str, readonly_fields: List[str], changes: Dict[str, Any] ) -> Dict[str, str]: """ Shortcut: Notification für READ-ONLY Felder """ return await notification_manager.notify_manual_action_required( entity_type='CAdressen', entity_id=address_entity_id, action_type='address_field_update_required', details={ 'betnr': betnr, 'readonly_fields': readonly_fields, 'changes': changes } )