feat(sync): Implement auto-reset for permanently_failed entities and add retry backoff logic

- Added logic to reset permanently_failed entities that have reached their auto-reset threshold in `beteiligte_sync_cron_step.py`.
- Enhanced event handling in `beteiligte_sync_event_step.py` to skip retries if the next retry time has not been reached.
- Introduced validation checks after sync operations to ensure data consistency and integrity.
- Created detailed documentation outlining the fixes and their impacts on the sync process.
- Added scripts for analyzing sync issues and comparing entities to facilitate debugging and validation.
This commit is contained in:
2026-02-08 21:12:00 +00:00
parent 6e0e9a9730
commit 79e097be6f
8 changed files with 1135 additions and 30 deletions

View File

@@ -116,6 +116,14 @@ class KommunikationSyncManager:
result['advoware_to_espocrm'] = espo_result
elif direction in ['both', 'to_espocrm'] and espo_wins:
self.logger.info(f"[KOMM] ⚠️ CONFLICT: EspoCRM wins - skipping Advoware→EspoCRM sync")
# FIX: Bei Konflikt müssen Var4-Einträge (neu in Advoware) zu Empty Slots gemacht werden!
# Sonst bleiben sie in Advoware aber nicht in EspoCRM → Nicht synchron!
self.logger.info(f"[KOMM] 🔄 Converting {len(diff['advo_new'])} Var4 entries to Empty Slots (EspoCRM wins)...")
for komm in diff['advo_new']:
await self._create_empty_slot(betnr, komm)
result['espocrm_to_advoware']['deleted'] += 1
else:
self.logger.info(f"[KOMM] Skipping Advoware→EspoCRM (direction={direction})")
@@ -147,13 +155,21 @@ class KommunikationSyncManager:
import hashlib
final_kommunikationen = advo_bet_final.get('kommunikation', [])
komm_rowids = sorted([k.get('rowId', '') for k in final_kommunikationen if k.get('rowId')])
# FIX #3: Nur sync-relevante Kommunikationen für Hash verwenden
# (nicht leere Slots oder nicht-sync-relevante Einträge)
sync_relevant_komm = [
k for k in final_kommunikationen
if should_sync_to_espocrm(k)
]
komm_rowids = sorted([k.get('rowId', '') for k in sync_relevant_komm if k.get('rowId')])
new_komm_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
await self.espocrm.update_entity('CBeteiligte', beteiligte_id, {
'kommunikationHash': new_komm_hash
})
self.logger.info(f"[KOMM] ✅ Updated kommunikationHash: {new_komm_hash}")
self.logger.info(f"[KOMM] ✅ Updated kommunikationHash: {new_komm_hash} (based on {len(sync_relevant_komm)} sync-relevant of {len(final_kommunikationen)} total)")
self.logger.info(f"[KOMM] ✅ Bidirectional Sync complete: {total_changes} total changes")
@@ -201,8 +217,13 @@ class KommunikationSyncManager:
last_sync = espo_bet.get('advowareLastSync')
# Berechne Hash aus Kommunikations-rowIds
# FIX #3: Nur sync-relevante Kommunikationen für Hash verwenden
import hashlib
komm_rowids = sorted([k.get('rowId', '') for k in advo_kommunikationen if k.get('rowId')])
sync_relevant_komm = [
k for k in advo_kommunikationen
if should_sync_to_espocrm(k)
]
komm_rowids = sorted([k.get('rowId', '') for k in sync_relevant_komm if k.get('rowId')])
current_advo_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]
stored_komm_hash = espo_bet.get('kommunikationHash')
@@ -534,17 +555,29 @@ class KommunikationSyncManager:
# ========== HELPER METHODS ==========
async def _create_empty_slot(self, betnr: int, advo_komm: Dict) -> None:
"""Erstellt leeren Slot für gelöschten Eintrag"""
"""
Erstellt leeren Slot für gelöschten Eintrag
Verwendet für:
- Var2: In EspoCRM gelöscht (hat Marker)
- Var4 bei Konflikt: Neu in Advoware aber EspoCRM wins (hat KEINEN Marker)
"""
try:
komm_id = advo_komm['id']
tlf = (advo_komm.get('tlf') or '').strip()
bemerkung = advo_komm.get('bemerkung') or ''
marker = parse_marker(bemerkung)
if not marker:
self.logger.warning(f"[KOMM] Kein Marker gefunden für gelöschten Eintrag: {komm_id}")
return
# Bestimme kommKz
if marker:
# Hat Marker (Var2)
kommkz = marker['kommKz']
else:
# Kein Marker (Var4 bei Konflikt) - erkenne kommKz aus Wert
from services.kommunikation_mapper import detect_kommkz
kommkz = detect_kommkz(tlf) if tlf else 1 # Default: TelGesch
self.logger.info(f"[KOMM] Var4 ohne Marker: erkenne kommKz={kommkz} aus Wert '{tlf[:20]}...'")
kommkz = marker['kommKz']
slot_marker = create_slot_marker(kommkz)
update_data = {