from services.advoware import AdvowareAPI from services.espocrm import EspoCRMAPI from services.bankverbindungen_mapper import BankverbindungenMapper from services.beteiligte_sync_utils import BeteiligteSync import json import redis from config import Config config = { 'type': 'event', 'name': 'VMH Bankverbindungen Sync Handler', 'description': 'Zentraler Sync-Handler für Bankverbindungen (Webhooks + Cron Events)', 'subscribes': [ 'vmh.bankverbindungen.create', 'vmh.bankverbindungen.update', 'vmh.bankverbindungen.delete', 'vmh.bankverbindungen.sync_check' ], 'flows': ['vmh'], 'emits': [] } async def handler(event_data, context): """ Zentraler Sync-Handler für Bankverbindungen Verarbeitet: - vmh.bankverbindungen.create: Neu in EspoCRM → Create in Advoware - vmh.bankverbindungen.update: Geändert in EspoCRM → Update in Advoware - vmh.bankverbindungen.delete: Gelöscht in EspoCRM → Delete in Advoware - vmh.bankverbindungen.sync_check: Cron-Check → Sync wenn nötig """ entity_id = event_data.get('entity_id') action = event_data.get('action', 'sync_check') source = event_data.get('source', 'unknown') if not entity_id: context.logger.error("Keine entity_id im Event gefunden") return context.logger.info(f"🔄 Bankverbindungen Sync gestartet: {action.upper()} | Entity: {entity_id} | Source: {source}") # Shared Redis client redis_client = redis.Redis( host=Config.REDIS_HOST, port=int(Config.REDIS_PORT), db=int(Config.REDIS_DB_ADVOWARE_CACHE), decode_responses=True ) # APIs initialisieren espocrm = EspoCRMAPI() advoware = AdvowareAPI(context) sync_utils = BeteiligteSync(espocrm, redis_client, context) # Reuse utils mapper = BankverbindungenMapper() 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: context.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: context.logger.error(f"❌ Fehler beim Laden von EspoCRM Entity: {e}") redis_client.delete(lock_key) return context.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: context.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: context.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, context, redis_client, lock_key) # FALL B: Existiert (hat advowareId) → UPDATE oder CHECK elif advoware_id: await handle_update(entity_id, betnr, advoware_id, espo_entity, espocrm, advoware, mapper, context, redis_client, lock_key) # FALL C: DELETE elif action == 'delete': await handle_delete(entity_id, betnr, advoware_id, espocrm, advoware, context, redis_client, lock_key) else: context.logger.warn(f"⚠️ Unbekannte Kombination: action={action}, advowareId={advoware_id}") redis_client.delete(lock_key) except Exception as e: context.logger.error(f"❌ Unerwarteter Fehler im Sync-Handler: {e}") import traceback context.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, context, redis_client, lock_key): """Erstellt neue Bankverbindung in Advoware""" try: context.logger.info(f"🔨 CREATE Bankverbindung in Advoware für Beteiligter {betnr}...") advo_data = mapper.map_cbankverbindungen_to_advoware(espo_entity) context.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', 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}") context.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) context.logger.info(f"✅ CREATE erfolgreich: {entity_id} → Advoware ID {new_id}") except Exception as e: context.logger.error(f"❌ CREATE fehlgeschlagen: {e}") redis_client.delete(lock_key) async def handle_update(entity_id, betnr, advoware_id, espo_entity, espocrm, advoware, mapper, context, redis_client, lock_key): """Update nicht möglich - Sendet Notification an User""" try: context.logger.warn(f"⚠️ UPDATE: Advoware API unterstützt kein PUT für Bankverbindungen") # Erstelle Notification für User in EspoCRM iban = espo_entity.get('iban', 'N/A') bank = espo_entity.get('bank', 'N/A') notification_message = ( f"Bankverbindung wurde in EspoCRM geändert, aber die Advoware API unterstützt keine Updates.\n\n" f"**Bitte manuell in Advoware aktualisieren:**\n" f"- Bank: {bank}\n" f"- IBAN: {iban}\n" f"- Beteiligter betNr: {betnr}\n" f"- Advoware ID: {advoware_id}\n\n" f"**Workaround:** Löschen und neu erstellen in EspoCRM, dann wird neue Bankverbindung in Advoware angelegt." ) # Sende Notification via EspoCRM API await espocrm.api_call('/Notification', method='POST', json_data={ 'type': 'message', 'message': notification_message, 'userId': espo_entity.get('createdById') or espo_entity.get('modifiedById'), 'relatedType': 'CBankverbindungen', 'relatedId': entity_id }) context.logger.info(f"📧 Notification an User gesendet: Manuelle Aktualisierung erforderlich") redis_client.delete(lock_key) except Exception as e: context.logger.error(f"❌ UPDATE Notification fehlgeschlagen: {e}") import traceback context.logger.error(traceback.format_exc()) redis_client.delete(lock_key) async def handle_delete(entity_id, betnr, advoware_id, espocrm, advoware, context, redis_client, lock_key): """Delete nicht möglich - Sendet Notification an User""" try: context.logger.warn(f"⚠️ DELETE: Advoware API unterstützt kein DELETE für Bankverbindungen") if not advoware_id: context.logger.info(f"ℹ️ Keine advowareId vorhanden, nur EspoCRM-seitiges Delete") redis_client.delete(lock_key) return # Hole Entity-Details für Notification (vor dem Delete) try: espo_entity = await espocrm.get_entity('CBankverbindungen', entity_id) iban = espo_entity.get('iban', 'N/A') bank = espo_entity.get('bank', 'N/A') user_id = espo_entity.get('createdById') or espo_entity.get('modifiedById') except: iban = 'N/A' bank = 'N/A' user_id = None # Erstelle Notification für User in EspoCRM notification_message = ( f"Bankverbindung wurde in EspoCRM gelöscht, aber die Advoware API unterstützt keine Löschungen.\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 bleibt in Advoware bestehen bis zur manuellen Löschung." ) # Sende Notification if user_id: await espocrm.api_call('/Notification', method='POST', json_data={ 'type': 'message', 'message': notification_message, 'userId': user_id }) context.logger.info(f"📧 Notification an User gesendet: Manuelle Löschung erforderlich") redis_client.delete(lock_key) except Exception as e: context.logger.error(f"❌ DELETE Notification fehlgeschlagen: {e}") redis_client.delete(lock_key)