""" VMH Bankverbindungen Sync Handler Zentraler Sync-Handler für Bankverbindungen (Webhooks + Cron Events) Verarbeitet: - vmh.bankverbindungen.create: Neu in EspoCRM → Create in Advoware - vmh.bankverbindungen.update: Geändert in EspoCRM → Notification (nicht unterstützt) - vmh.bankverbindungen.delete: Gelöscht in EspoCRM → Notification (nicht unterstützt) - vmh.bankverbindungen.sync_check: Cron-Check → Sync wenn nötig """ from typing import Dict, Any, Optional from motia import FlowContext, queue from services.advoware import AdvowareAPI from services.espocrm import EspoCRMAPI from services.bankverbindungen_mapper import BankverbindungenMapper from services.notification_utils import NotificationManager from services.redis_client import get_redis_client import json config = { "name": "VMH Bankverbindungen Sync Handler", "description": "Zentraler Sync-Handler für Bankverbindungen (Webhooks + Cron Events)", "flows": ["vmh-bankverbindungen"], "triggers": [ queue("vmh.bankverbindungen.create"), queue("vmh.bankverbindungen.update"), queue("vmh.bankverbindungen.delete"), queue("vmh.bankverbindungen.sync_check") ], "enqueues": [] } async def handler(event_data: Dict[str, Any], ctx: FlowContext[Any]) -> None: """Zentraler Sync-Handler für Bankverbindungen""" entity_id = event_data.get('entity_id') action = event_data.get('action', 'sync_check') source = event_data.get('source', 'unknown') if not entity_id: ctx.logger.error("Keine entity_id im Event gefunden") return ctx.logger.info(f"🔄 Bankverbindungen Sync gestartet: {action.upper()} | Entity: {entity_id} | Source: {source}") # Shared Redis client (centralized factory) redis_client = get_redis_client(strict=False) # APIs initialisieren (mit Context für besseres Logging) espocrm = EspoCRMAPI(ctx) advoware = AdvowareAPI(ctx) mapper = BankverbindungenMapper() notification_mgr = NotificationManager(espocrm_api=espocrm, context=ctx) try: # 1. ACQUIRE LOCK lock_key = f"sync_lock:cbankverbindungen:{entity_id}" acquired = redis_client.set(lock_key, "locked", nx=True, ex=900) # 15min TTL if not acquired: ctx.logger.warn(f"⏸️ Sync bereits aktiv für {entity_id}, überspringe") return # 2. FETCH ENTITY VON ESPOCRM try: espo_entity = await espocrm.get_entity('CBankverbindungen', entity_id) except Exception as e: ctx.logger.error(f"❌ Fehler beim Laden von EspoCRM Entity: {e}") redis_client.delete(lock_key) return ctx.logger.info(f"📋 Entity geladen: {espo_entity.get('name', 'Unbenannt')} (IBAN: {espo_entity.get('iban', 'N/A')})") advoware_id = espo_entity.get('advowareId') beteiligte_id = espo_entity.get('cBeteiligteId') # Parent Beteiligter if not beteiligte_id: ctx.logger.error(f"❌ Keine cBeteiligteId gefunden - Bankverbindung muss einem Beteiligten zugeordnet sein") redis_client.delete(lock_key) return # Hole betNr vom Parent parent = await espocrm.get_entity('CBeteiligte', beteiligte_id) betnr = parent.get('betnr') if not betnr: ctx.logger.error(f"❌ Parent Beteiligter {beteiligte_id} hat keine betNr") redis_client.delete(lock_key) return # 3. BESTIMME SYNC-AKTION # FALL A: Neu (kein advowareId) → CREATE in Advoware if not advoware_id and action in ['create', 'sync_check']: await handle_create(entity_id, betnr, espo_entity, espocrm, advoware, mapper, ctx, redis_client, lock_key) # FALL B: Existiert (hat advowareId) → UPDATE oder CHECK (nicht unterstützt!) elif advoware_id and action in ['update', 'sync_check']: await handle_update(entity_id, betnr, advoware_id, espo_entity, espocrm, notification_mgr, ctx, redis_client, lock_key) # FALL C: DELETE (nicht unterstützt!) elif action == 'delete': await handle_delete(entity_id, betnr, advoware_id, espo_entity, espocrm, notification_mgr, ctx, redis_client, lock_key) else: ctx.logger.warn(f"⚠️ Unbekannte Kombination: action={action}, advowareId={advoware_id}") redis_client.delete(lock_key) except Exception as e: ctx.logger.error(f"❌ Unerwarteter Fehler im Sync-Handler: {e}") import traceback ctx.logger.error(traceback.format_exc()) try: redis_client.delete(lock_key) except: pass async def handle_create(entity_id, betnr, espo_entity, espocrm, advoware, mapper, ctx, redis_client, lock_key) -> None: """Erstellt neue Bankverbindung in Advoware""" try: ctx.logger.info(f"🔨 CREATE Bankverbindung in Advoware für Beteiligter {betnr}...") advo_data = mapper.map_cbankverbindungen_to_advoware(espo_entity) ctx.logger.info(f"📤 Sende an Advoware: {json.dumps(advo_data, ensure_ascii=False)[:200]}...") # POST zu Advoware (Beteiligten-spezifischer Endpoint!) result = await advoware.api_call( f'api/v1/advonet/Beteiligte/{betnr}/Bankverbindungen', method='POST', json_data=advo_data ) # Extrahiere ID und rowId if isinstance(result, list) and len(result) > 0: new_entity = result[0] elif isinstance(result, dict): new_entity = result else: raise Exception(f"Unexpected response format: {result}") new_id = new_entity.get('id') new_rowid = new_entity.get('rowId') if not new_id: raise Exception(f"Keine ID in Advoware Response: {result}") ctx.logger.info(f"✅ In Advoware erstellt: ID={new_id}, rowId={new_rowid[:20] if new_rowid else 'N/A'}...") # Schreibe advowareId + rowId zurück await espocrm.update_entity('CBankverbindungen', entity_id, { 'advowareId': new_id, 'advowareRowId': new_rowid }) redis_client.delete(lock_key) ctx.logger.info(f"✅ CREATE erfolgreich: {entity_id} → Advoware ID {new_id}") except Exception as e: ctx.logger.error(f"❌ CREATE fehlgeschlagen: {e}") redis_client.delete(lock_key) async def handle_update(entity_id, betnr, advoware_id, espo_entity, espocrm, notification_mgr, ctx, redis_client, lock_key) -> None: """Update nicht möglich - Sendet Notification an User""" try: ctx.logger.warn(f"⚠️ UPDATE: Advoware API unterstützt kein PUT für Bankverbindungen") iban = espo_entity.get('iban', 'N/A') bank = espo_entity.get('bank', 'N/A') name = espo_entity.get('name', 'Unbenannt') # Sende Notification await notification_mgr.notify_manual_action_required( entity_type='CBankverbindungen', entity_id=entity_id, action_type='general_manual_action', details={ 'message': f'UPDATE nicht möglich für Bankverbindung: {name}', 'description': ( f"Die Advoware API unterstützt keine Updates für Bankverbindungen.\n\n" f"**Details:**\n" f"- Bank: {bank}\n" f"- IBAN: {iban}\n" f"- Beteiligter betNr: {betnr}\n" f"- Advoware ID: {advoware_id}\n\n" f"**Workaround:**\n" f"Löschen Sie die Bankverbindung in EspoCRM und erstellen Sie sie neu. " f"Die neue Bankverbindung wird dann automatisch in Advoware angelegt." ), 'entity_name': name, 'priority': 'Normal' }, create_task=True ) ctx.logger.info(f"📧 Notification gesendet: Update-Limitation") redis_client.delete(lock_key) except Exception as e: ctx.logger.error(f"❌ UPDATE Notification fehlgeschlagen: {e}") import traceback ctx.logger.error(traceback.format_exc()) redis_client.delete(lock_key) async def handle_delete(entity_id, betnr, advoware_id, espo_entity, espocrm, notification_mgr, ctx, redis_client, lock_key) -> None: """Delete nicht möglich - Sendet Notification an User""" try: ctx.logger.warn(f"⚠️ DELETE: Advoware API unterstützt kein DELETE für Bankverbindungen") if not advoware_id: ctx.logger.info(f"ℹ️ Keine advowareId vorhanden, nur EspoCRM-seitiges Delete") redis_client.delete(lock_key) return iban = espo_entity.get('iban', 'N/A') bank = espo_entity.get('bank', 'N/A') name = espo_entity.get('name', 'Unbenannt') # Sende Notification await notification_mgr.notify_manual_action_required( entity_type='CBankverbindungen', entity_id=entity_id, action_type='general_manual_action', details={ 'message': f'DELETE erforderlich für Bankverbindung: {name}', 'description': ( f"Die Advoware API unterstützt keine Löschungen für Bankverbindungen.\n\n" f"**Bitte manuell in Advoware löschen:**\n" f"- Bank: {bank}\n" f"- IBAN: {iban}\n" f"- Beteiligter betNr: {betnr}\n" f"- Advoware ID: {advoware_id}\n\n" f"Die Bankverbindung wurde in EspoCRM gelöscht, bleibt aber in Advoware " f"bestehen bis zur manuellen Löschung." ), 'entity_name': name, 'priority': 'Normal' }, create_task=True ) ctx.logger.info(f"📧 Notification gesendet: Delete erforderlich") redis_client.delete(lock_key) except Exception as e: ctx.logger.error(f"❌ DELETE Notification fehlgeschlagen: {e}") redis_client.delete(lock_key)