277 lines
11 KiB
Python
277 lines
11 KiB
Python
from services.advoware import AdvowareAPI
|
|
from services.espocrm import EspoCRMAPI
|
|
from services.espocrm_mapper import BeteiligteMapper
|
|
from services.beteiligte_sync_utils import BeteiligteSync
|
|
import json
|
|
import redis
|
|
from config import Config
|
|
|
|
config = {
|
|
'type': 'event',
|
|
'name': 'VMH Beteiligte Sync Handler',
|
|
'description': 'Zentraler Sync-Handler für Beteiligte (Webhooks + Cron Events)',
|
|
'subscribes': [
|
|
'vmh.beteiligte.create',
|
|
'vmh.beteiligte.update',
|
|
'vmh.beteiligte.delete',
|
|
'vmh.beteiligte.sync_check' # Von Cron
|
|
],
|
|
'flows': ['vmh'],
|
|
'emits': []
|
|
}
|
|
|
|
async def handler(event_data, context):
|
|
"""
|
|
Zentraler Sync-Handler für Beteiligte
|
|
|
|
Verarbeitet:
|
|
- vmh.beteiligte.create: Neu in EspoCRM → Create in Advoware
|
|
- vmh.beteiligte.update: Geändert in EspoCRM → Update in Advoware
|
|
- vmh.beteiligte.delete: Gelöscht in EspoCRM → Delete in Advoware
|
|
- vmh.beteiligte.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"🔄 Sync-Handler gestartet: {action.upper()} | Entity: {entity_id} | Source: {source}")
|
|
|
|
# Shared Redis client for distributed locking
|
|
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)
|
|
mapper = BeteiligteMapper()
|
|
|
|
try:
|
|
# 1. ACQUIRE LOCK (verhindert parallele Syncs)
|
|
lock_acquired = await sync_utils.acquire_sync_lock(entity_id)
|
|
|
|
if not lock_acquired:
|
|
context.logger.warning(f"⏸️ Sync bereits aktiv für {entity_id}, überspringe")
|
|
return
|
|
|
|
# 2. FETCH ENTITY VON ESPOCRM
|
|
try:
|
|
espo_entity = await espocrm.get_entity('CBeteiligte', entity_id)
|
|
except Exception as e:
|
|
context.logger.error(f"❌ Fehler beim Laden von EspoCRM Entity: {e}")
|
|
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True)
|
|
return
|
|
|
|
context.logger.info(f"📋 Entity geladen: {espo_entity.get('name')} (betnr: {espo_entity.get('betnr')})")
|
|
|
|
betnr = espo_entity.get('betnr')
|
|
sync_status = espo_entity.get('syncStatus', 'pending_sync')
|
|
|
|
# 3. BESTIMME SYNC-AKTION
|
|
|
|
# FALL A: Neu (kein betnr) → CREATE in Advoware
|
|
if not betnr and action in ['create', 'sync_check']:
|
|
context.logger.info(f"🆕 Neuer Beteiligter → CREATE in Advoware")
|
|
await handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, mapper, context)
|
|
|
|
# FALL B: Existiert (hat betnr) → UPDATE oder CHECK
|
|
elif betnr:
|
|
context.logger.info(f"♻️ Existierender Beteiligter (betNr: {betnr}) → UPDATE/CHECK")
|
|
await handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, context)
|
|
|
|
# FALL C: DELETE (TODO: Implementierung später)
|
|
elif action == 'delete':
|
|
context.logger.warning(f"🗑️ DELETE noch nicht implementiert für {entity_id}")
|
|
await sync_utils.release_sync_lock(entity_id, 'failed', 'Delete-Operation nicht implementiert')
|
|
|
|
else:
|
|
context.logger.warning(f"⚠️ Unbekannte Kombination: action={action}, betnr={betnr}")
|
|
await sync_utils.release_sync_lock(entity_id, 'failed', f'Unbekannte Aktion: {action}')
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"❌ Unerwarteter Fehler im Sync-Handler: {e}")
|
|
import traceback
|
|
context.logger.error(traceback.format_exc())
|
|
|
|
try:
|
|
await sync_utils.release_sync_lock(
|
|
entity_id,
|
|
'failed',
|
|
f'Unerwarteter Fehler: {str(e)[:1900]}',
|
|
increment_retry=True
|
|
)
|
|
except:
|
|
pass
|
|
|
|
|
|
async def handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, mapper, context):
|
|
"""Erstellt neuen Beteiligten in Advoware"""
|
|
try:
|
|
context.logger.info(f"🔨 CREATE in Advoware...")
|
|
|
|
# Transform zu Advoware Format
|
|
advo_data = mapper.map_cbeteiligte_to_advoware(espo_entity)
|
|
|
|
context.logger.info(f"📤 Sende an Advoware: {json.dumps(advo_data, ensure_ascii=False)[:200]}...")
|
|
|
|
# POST zu Advoware
|
|
result = await advoware.api_call(
|
|
'api/v1/advonet/Beteiligte',
|
|
method='POST',
|
|
data=advo_data
|
|
)
|
|
|
|
# Extrahiere betNr aus Response
|
|
new_betnr = result.get('betNr') if isinstance(result, dict) else None
|
|
|
|
if not new_betnr:
|
|
raise Exception(f"Keine betNr in Advoware Response: {result}")
|
|
|
|
context.logger.info(f"✅ In Advoware erstellt: betNr={new_betnr}")
|
|
|
|
# OPTIMIERT: Kombiniere release_lock + betnr update in 1 API call
|
|
await sync_utils.release_sync_lock(
|
|
entity_id,
|
|
'clean',
|
|
error_message=None,
|
|
extra_fields={'betnr': new_betnr}
|
|
)
|
|
|
|
context.logger.info(f"✅ CREATE erfolgreich: {entity_id} → betNr {new_betnr}")
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"❌ CREATE fehlgeschlagen: {e}")
|
|
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True)
|
|
|
|
|
|
async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, context):
|
|
"""Synchronisiert existierenden Beteiligten"""
|
|
try:
|
|
context.logger.info(f"🔍 Fetch von Advoware betNr={betnr}...")
|
|
|
|
# Fetch von Advoware
|
|
try:
|
|
advo_result = await advoware.api_call(
|
|
f'api/v1/advonet/Beteiligte/{betnr}',
|
|
method='GET'
|
|
)
|
|
|
|
# Advoware gibt manchmal Listen zurück
|
|
if isinstance(advo_result, list):
|
|
advo_entity = advo_result[0] if advo_result else None
|
|
else:
|
|
advo_entity = advo_result
|
|
|
|
if not advo_entity:
|
|
raise Exception(f"Beteiligter betNr={betnr} nicht gefunden")
|
|
|
|
except Exception as e:
|
|
# 404 oder anderer Fehler → Beteiligter wurde in Advoware gelöscht
|
|
if '404' in str(e) or 'nicht gefunden' in str(e).lower():
|
|
context.logger.warning(f"🗑️ Beteiligter in Advoware gelöscht: betNr={betnr}")
|
|
await sync_utils.handle_advoware_deleted(entity_id, str(e))
|
|
return
|
|
else:
|
|
raise
|
|
|
|
context.logger.info(f"📥 Von Advoware geladen: {advo_entity.get('name')}")
|
|
|
|
# TIMESTAMP-VERGLEICH
|
|
comparison = sync_utils.compare_timestamps(
|
|
espo_entity.get('modifiedAt'),
|
|
advo_entity.get('geaendertAm'),
|
|
espo_entity.get('advowareLastSync')
|
|
)
|
|
|
|
context.logger.info(f"⏱️ Timestamp-Vergleich: {comparison}")
|
|
|
|
# SPECIAL: Wenn LastSync null → immer von EspoCRM syncen (initial sync)
|
|
if not espo_entity.get('advowareLastSync'):
|
|
context.logger.info(f"📤 Initial Sync → EspoCRM STAMMDATEN zu Advoware")
|
|
|
|
# OPTIMIERT: Use merge utility (reduces code duplication)
|
|
merged_data = sync_utils.merge_for_advoware_put(advo_entity, espo_entity, mapper)
|
|
|
|
await advoware.api_call(
|
|
f'api/v1/advonet/Beteiligte/{betnr}',
|
|
method='PUT',
|
|
data=merged_data
|
|
)
|
|
|
|
await sync_utils.release_sync_lock(entity_id, 'clean')
|
|
context.logger.info(f"✅ Advoware aktualisiert (initial sync)")
|
|
return
|
|
|
|
# KEIN SYNC NÖTIG
|
|
if comparison == 'no_change':
|
|
context.logger.info(f"✅ Keine Änderungen, Sync übersprungen")
|
|
await sync_utils.release_sync_lock(entity_id, 'clean')
|
|
return
|
|
|
|
# ESPOCRM NEUER → Update Advoware
|
|
if comparison == 'espocrm_newer':
|
|
context.logger.info(f"📤 EspoCRM ist neuer → Update Advoware STAMMDATEN")
|
|
|
|
# OPTIMIERT: Use merge utility
|
|
merged_data = sync_utils.merge_for_advoware_put(advo_entity, espo_entity, mapper)
|
|
|
|
await advoware.api_call(
|
|
f'api/v1/advonet/Beteiligte/{betnr}',
|
|
method='PUT',
|
|
data=merged_data
|
|
)
|
|
|
|
await sync_utils.release_sync_lock(entity_id, 'clean')
|
|
context.logger.info(f"✅ Advoware aktualisiert")
|
|
|
|
# ADVOWARE NEUER → Update EspoCRM
|
|
elif comparison == 'advoware_newer':
|
|
context.logger.info(f"📥 Advoware ist neuer → Update EspoCRM STAMMDATEN")
|
|
|
|
espo_data = mapper.map_advoware_to_cbeteiligte(advo_entity)
|
|
|
|
await espocrm.update_entity('CBeteiligte', entity_id, espo_data)
|
|
await sync_utils.release_sync_lock(entity_id, 'clean')
|
|
context.logger.info(f"✅ EspoCRM aktualisiert")
|
|
|
|
# KONFLIKT → EspoCRM WINS
|
|
elif comparison == 'conflict':
|
|
context.logger.warning(f"⚠️ KONFLIKT erkannt → EspoCRM WINS (STAMMDATEN)")
|
|
|
|
# OPTIMIERT: Use merge utility
|
|
merged_data = sync_utils.merge_for_advoware_put(advo_entity, espo_entity, mapper)
|
|
|
|
await advoware.api_call(
|
|
f'api/v1/advonet/Beteiligte/{betnr}',
|
|
method='PUT',
|
|
data=merged_data
|
|
)
|
|
|
|
conflict_msg = (
|
|
f"EspoCRM: {espo_entity.get('modifiedAt')}, "
|
|
f"Advoware: {advo_entity.get('geaendertAm')}. "
|
|
f"EspoCRM hat gewonnen."
|
|
)
|
|
|
|
await sync_utils.resolve_conflict_espocrm_wins(
|
|
entity_id,
|
|
espo_entity,
|
|
advo_entity,
|
|
conflict_msg
|
|
)
|
|
|
|
context.logger.info(f"✅ Konflikt gelöst: EspoCRM → Advoware")
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"❌ UPDATE fehlgeschlagen: {e}")
|
|
import traceback
|
|
context.logger.error(traceback.format_exc())
|
|
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True) |