feat: Implement VMH Bankverbindungen sync handlers and API steps for create, update, and delete operations

This commit is contained in:
2026-02-08 12:49:14 +00:00
parent 709456301c
commit 68c8b398aa
7 changed files with 649 additions and 21 deletions

View File

@@ -0,0 +1,251 @@
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)