feat: Implement VMH Bankverbindungen sync handlers and API steps for create, update, and delete operations
This commit is contained in:
251
bitbylaw/steps/vmh/bankverbindungen_sync_event_step.py
Normal file
251
bitbylaw/steps/vmh/bankverbindungen_sync_event_step.py
Normal 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)
|
||||
@@ -0,0 +1,61 @@
|
||||
import json
|
||||
import datetime
|
||||
|
||||
config = {
|
||||
'type': 'api',
|
||||
'name': 'VMH Webhook Bankverbindungen Create',
|
||||
'description': 'Empfängt Create-Webhooks von EspoCRM für Bankverbindungen',
|
||||
'path': '/vmh/webhook/bankverbindungen/create',
|
||||
'method': 'POST',
|
||||
'flows': ['vmh'],
|
||||
'emits': ['vmh.bankverbindungen.create']
|
||||
}
|
||||
|
||||
async def handler(req, context):
|
||||
try:
|
||||
payload = req.get('body', [])
|
||||
|
||||
context.logger.info("VMH Webhook Bankverbindungen Create empfangen")
|
||||
context.logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")
|
||||
|
||||
# Sammle alle IDs aus dem Batch
|
||||
entity_ids = set()
|
||||
|
||||
if isinstance(payload, list):
|
||||
for entity in payload:
|
||||
if isinstance(entity, dict) and 'id' in entity:
|
||||
entity_ids.add(entity['id'])
|
||||
elif isinstance(payload, dict) and 'id' in payload:
|
||||
entity_ids.add(payload['id'])
|
||||
|
||||
context.logger.info(f"{len(entity_ids)} IDs zum Create-Sync gefunden")
|
||||
|
||||
# Emittiere Events
|
||||
for entity_id in entity_ids:
|
||||
await context.emit({
|
||||
'topic': 'vmh.bankverbindungen.create',
|
||||
'data': {
|
||||
'entity_id': entity_id,
|
||||
'action': 'create',
|
||||
'source': 'webhook',
|
||||
'timestamp': req.get('timestamp') or datetime.datetime.now().isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
context.logger.info(f"VMH Create Webhook verarbeitet: {len(entity_ids)} Events emittiert")
|
||||
|
||||
return {
|
||||
'status': 200,
|
||||
'body': {
|
||||
'status': 'received',
|
||||
'action': 'create',
|
||||
'ids_count': len(entity_ids)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
context.logger.error(f"Fehler beim Verarbeiten des VMH Create Webhooks: {e}")
|
||||
return {
|
||||
'status': 500,
|
||||
'body': {'error': str(e)}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import json
|
||||
import datetime
|
||||
|
||||
config = {
|
||||
'type': 'api',
|
||||
'name': 'VMH Webhook Bankverbindungen Delete',
|
||||
'description': 'Empfängt Delete-Webhooks von EspoCRM für Bankverbindungen',
|
||||
'path': '/vmh/webhook/bankverbindungen/delete',
|
||||
'method': 'POST',
|
||||
'flows': ['vmh'],
|
||||
'emits': ['vmh.bankverbindungen.delete']
|
||||
}
|
||||
|
||||
async def handler(req, context):
|
||||
try:
|
||||
payload = req.get('body', [])
|
||||
|
||||
context.logger.info("VMH Webhook Bankverbindungen Delete empfangen")
|
||||
context.logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")
|
||||
|
||||
# Sammle alle IDs
|
||||
entity_ids = set()
|
||||
|
||||
if isinstance(payload, list):
|
||||
for entity in payload:
|
||||
if isinstance(entity, dict) and 'id' in entity:
|
||||
entity_ids.add(entity['id'])
|
||||
elif isinstance(payload, dict) and 'id' in payload:
|
||||
entity_ids.add(payload['id'])
|
||||
|
||||
context.logger.info(f"{len(entity_ids)} IDs zum Delete-Sync gefunden")
|
||||
|
||||
# Emittiere Events
|
||||
for entity_id in entity_ids:
|
||||
await context.emit({
|
||||
'topic': 'vmh.bankverbindungen.delete',
|
||||
'data': {
|
||||
'entity_id': entity_id,
|
||||
'action': 'delete',
|
||||
'source': 'webhook',
|
||||
'timestamp': req.get('timestamp') or datetime.datetime.now().isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
context.logger.info(f"VMH Delete Webhook verarbeitet: {len(entity_ids)} Events emittiert")
|
||||
|
||||
return {
|
||||
'status': 200,
|
||||
'body': {
|
||||
'status': 'received',
|
||||
'action': 'delete',
|
||||
'ids_count': len(entity_ids)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
context.logger.error(f"Fehler beim Verarbeiten des VMH Delete Webhooks: {e}")
|
||||
return {
|
||||
'status': 500,
|
||||
'body': {'error': str(e)}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import json
|
||||
import datetime
|
||||
|
||||
config = {
|
||||
'type': 'api',
|
||||
'name': 'VMH Webhook Bankverbindungen Update',
|
||||
'description': 'Empfängt Update-Webhooks von EspoCRM für Bankverbindungen',
|
||||
'path': '/vmh/webhook/bankverbindungen/update',
|
||||
'method': 'POST',
|
||||
'flows': ['vmh'],
|
||||
'emits': ['vmh.bankverbindungen.update']
|
||||
}
|
||||
|
||||
async def handler(req, context):
|
||||
try:
|
||||
payload = req.get('body', [])
|
||||
|
||||
context.logger.info("VMH Webhook Bankverbindungen Update empfangen")
|
||||
context.logger.info(f"Payload: {json.dumps(payload, indent=2, ensure_ascii=False)}")
|
||||
|
||||
# Sammle alle IDs
|
||||
entity_ids = set()
|
||||
|
||||
if isinstance(payload, list):
|
||||
for entity in payload:
|
||||
if isinstance(entity, dict) and 'id' in entity:
|
||||
entity_ids.add(entity['id'])
|
||||
elif isinstance(payload, dict) and 'id' in payload:
|
||||
entity_ids.add(payload['id'])
|
||||
|
||||
context.logger.info(f"{len(entity_ids)} IDs zum Update-Sync gefunden")
|
||||
|
||||
# Emittiere Events
|
||||
for entity_id in entity_ids:
|
||||
await context.emit({
|
||||
'topic': 'vmh.bankverbindungen.update',
|
||||
'data': {
|
||||
'entity_id': entity_id,
|
||||
'action': 'update',
|
||||
'source': 'webhook',
|
||||
'timestamp': req.get('timestamp') or datetime.datetime.now().isoformat()
|
||||
}
|
||||
})
|
||||
|
||||
context.logger.info(f"VMH Update Webhook verarbeitet: {len(entity_ids)} Events emittiert")
|
||||
|
||||
return {
|
||||
'status': 200,
|
||||
'body': {
|
||||
'status': 'received',
|
||||
'action': 'update',
|
||||
'ids_count': len(entity_ids)
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
context.logger.error(f"Fehler beim Verarbeiten des VMH Update Webhooks: {e}")
|
||||
return {
|
||||
'status': 500,
|
||||
'body': {'error': str(e)}
|
||||
}
|
||||
Reference in New Issue
Block a user