255 lines
10 KiB
Python
255 lines
10 KiB
Python
"""
|
||
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)
|