feat: Implement bidirectional synchronization utilities for Advoware and EspoCRM communications
- Added KommunikationSyncManager class to handle synchronization logic. - Implemented methods for loading data, computing diffs, and applying changes between Advoware and EspoCRM. - Introduced 3-way diffing mechanism to intelligently resolve conflicts. - Added helper methods for creating empty slots and detecting changes in communications. - Enhanced logging for better traceability during synchronization processes.
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
from typing import Dict, Any, Optional
|
||||
from services.advoware import AdvowareAPI
|
||||
from services.advoware_service import AdvowareService
|
||||
from services.espocrm import EspoCRMAPI
|
||||
from services.espocrm_mapper import BeteiligteMapper
|
||||
from services.beteiligte_sync_utils import BeteiligteSync
|
||||
from services.kommunikation_sync_utils import (
|
||||
KommunikationSyncManager,
|
||||
detect_kommunikation_changes
|
||||
)
|
||||
import json
|
||||
import redis
|
||||
from config import Config
|
||||
@@ -54,6 +60,10 @@ async def handler(event_data, context):
|
||||
sync_utils = BeteiligteSync(espocrm, redis_client, context)
|
||||
mapper = BeteiligteMapper()
|
||||
|
||||
# Kommunikation Sync Manager
|
||||
advo_service = AdvowareService(context)
|
||||
komm_sync = KommunikationSyncManager(advo_service, espocrm, context)
|
||||
|
||||
try:
|
||||
# 1. ACQUIRE LOCK (verhindert parallele Syncs)
|
||||
lock_acquired = await sync_utils.acquire_sync_lock(entity_id)
|
||||
@@ -85,7 +95,7 @@ async def handler(event_data, 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)
|
||||
await handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, komm_sync, context)
|
||||
|
||||
# FALL C: DELETE (TODO: Implementierung später)
|
||||
elif action == 'delete':
|
||||
@@ -112,6 +122,28 @@ async def handler(event_data, context):
|
||||
pass
|
||||
|
||||
|
||||
async def run_kommunikation_sync(entity_id: str, betnr: int, komm_sync, context, direction: str = 'both') -> Dict[str, Any]:
|
||||
"""
|
||||
Helper: Führt Kommunikation-Sync aus mit Error-Handling
|
||||
|
||||
Args:
|
||||
direction: 'both' (bidirektional), 'to_advoware' (nur EspoCRM→Advoware), 'to_espocrm' (nur Advoware→EspoCRM)
|
||||
|
||||
Returns:
|
||||
Sync-Ergebnis oder None bei Fehler
|
||||
"""
|
||||
context.logger.info(f"📞 Starte Kommunikation-Sync (direction={direction})...")
|
||||
try:
|
||||
komm_result = await komm_sync.sync_bidirectional(entity_id, betnr, direction=direction)
|
||||
context.logger.info(f"✅ Kommunikation synced: {komm_result}")
|
||||
return komm_result
|
||||
except Exception as e:
|
||||
context.logger.error(f"⚠️ Kommunikation-Sync fehlgeschlagen: {e}")
|
||||
import traceback
|
||||
context.logger.error(traceback.format_exc())
|
||||
return None
|
||||
|
||||
|
||||
async def handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, mapper, context):
|
||||
"""Erstellt neuen Beteiligten in Advoware"""
|
||||
try:
|
||||
@@ -167,7 +199,7 @@ async def handle_create(entity_id, espo_entity, espocrm, advoware, sync_utils, m
|
||||
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):
|
||||
async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_utils, mapper, komm_sync, context):
|
||||
"""Synchronisiert existierenden Beteiligten"""
|
||||
try:
|
||||
context.logger.info(f"🔍 Fetch von Advoware betNr={betnr}...")
|
||||
@@ -204,9 +236,18 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
||||
|
||||
context.logger.info(f"⏱️ Vergleich: {comparison}")
|
||||
|
||||
# KEIN SYNC NÖTIG
|
||||
# KOMMUNIKATION-ÄNDERUNGSERKENNUNG (zusätzlich zu Stammdaten)
|
||||
# Speichere alte Version für späteren Vergleich
|
||||
old_advo_entity = advo_entity.copy()
|
||||
komm_changes_detected = False
|
||||
|
||||
# KEIN STAMMDATEN-SYNC NÖTIG (aber Kommunikation könnte geändert sein)
|
||||
if comparison == 'no_change':
|
||||
context.logger.info(f"✅ Keine Änderungen, Sync übersprungen")
|
||||
context.logger.info(f"✅ Keine Stammdaten-Änderungen erkannt")
|
||||
|
||||
# KOMMUNIKATION SYNC: Prüfe trotzdem Kommunikationen
|
||||
await run_kommunikation_sync(entity_id, betnr, komm_sync, context)
|
||||
|
||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
||||
return
|
||||
|
||||
@@ -230,27 +271,35 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
||||
elif isinstance(put_result, dict):
|
||||
new_rowid = put_result.get('rowId')
|
||||
|
||||
# Release Lock + Update rowId in einem Call (effizienter!)
|
||||
context.logger.info(f"✅ Advoware STAMMDATEN aktualisiert, rowId: {new_rowid[:20] if new_rowid else 'N/A'}...")
|
||||
|
||||
# KOMMUNIKATION SYNC: Immer ausführen nach Stammdaten-Update
|
||||
await run_kommunikation_sync(entity_id, betnr, komm_sync, context)
|
||||
|
||||
# Release Lock NACH Kommunikation-Sync + Update rowId
|
||||
await sync_utils.release_sync_lock(
|
||||
entity_id,
|
||||
'clean',
|
||||
extra_fields={'advowareRowId': new_rowid}
|
||||
)
|
||||
context.logger.info(f"✅ Advoware aktualisiert, rowId in EspoCRM geschrieben: {new_rowid[:20] if new_rowid else 'N/A'}...")
|
||||
|
||||
# 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)
|
||||
context.logger.info(f"✅ EspoCRM STAMMDATEN aktualisiert")
|
||||
|
||||
# KOMMUNIKATION SYNC: Immer ausführen nach Stammdaten-Update
|
||||
await run_kommunikation_sync(entity_id, betnr, komm_sync, context)
|
||||
|
||||
# Release Lock NACH Kommunikation-Sync + Update rowId
|
||||
await sync_utils.release_sync_lock(
|
||||
entity_id,
|
||||
'clean',
|
||||
extra_fields={'advowareRowId': advo_entity.get('rowId')}
|
||||
)
|
||||
context.logger.info(f"✅ EspoCRM aktualisiert")
|
||||
|
||||
# KONFLIKT → EspoCRM WINS
|
||||
elif comparison == 'conflict':
|
||||
@@ -286,9 +335,19 @@ async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, sync_u
|
||||
extra_fields={'advowareRowId': new_rowid}
|
||||
)
|
||||
context.logger.info(f"✅ Konflikt gelöst (EspoCRM won), neue rowId: {new_rowid[:20] if new_rowid else 'N/A'}...")
|
||||
|
||||
# KOMMUNIKATION SYNC: NUR EspoCRM→Advoware (EspoCRM wins!)
|
||||
await run_kommunikation_sync(entity_id, betnr, komm_sync, context, direction='to_advoware')
|
||||
|
||||
# Release Lock NACH Kommunikation-Sync
|
||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
||||
|
||||
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)
|
||||
await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True)
|
||||
|
||||
|
||||
# Alias für Tests/externe Aufrufe
|
||||
handle = handler
|
||||
Reference in New Issue
Block a user