# Analyse: syncStatus Werte in EspoCRM CBeteiligte ## Datum: 8. Februar 2026 (Updated) ## Design-Philosophie: Defense in Depth (Webhook + Cron Fallback) Das System verwendet **zwei parallele Sync-Trigger**: 1. **Primary Path (Webhook)**: Echtzeit-Sync bei Änderungen in EspoCRM 2. **Fallback Path (Cron)**: 15-Minuten-Check falls Webhook fehlschlägt Dies garantiert robuste Synchronisation auch bei temporären Webhook-Ausfällen. --- ## Übersicht: Definierte syncStatus-Werte Basierend auf Code-Analyse wurden folgende Status identifiziert: | Status | Bedeutung | Gesetzt von | Zweck | |--------|-----------|-------------|-------| | `pending_sync` | Wartet auf ersten Sync | **EspoCRM** (bei CREATE) | Cron-Fallback wenn Webhook fehlschlägt | | `dirty` | Daten geändert, Sync nötig | **EspoCRM** (bei UPDATE) | Cron-Fallback wenn Webhook fehlschlägt | | `syncing` | Sync läuft gerade | **Python** (acquire_lock) | Lock während Sync | | `clean` | Erfolgreich synchronisiert | **Python** (release_lock) | Sync erfolgreich | | `failed` | Sync fehlgeschlagen (< 5 Retries) | **Python** (bei Fehler) | Retry mit Backoff | | `permanently_failed` | Sync fehlgeschlagen (≥ 5 Retries) | **Python** (max retries) | Auto-Reset nach 24h | | `conflict` | Konflikt erkannt (optional) | **Python** (bei Konflikt) | UI-Visibility für Konflikte | | `deleted_in_advoware` | In Advoware gelöscht (404) | **Python** (bei 404) | Soft-Delete Strategie | ### Status-Verantwortlichkeiten **EspoCRM Verantwortung** (Frontend/Hooks): - `pending_sync` - Bei CREATE neuer CBeteiligte Entity - `dirty` - Bei UPDATE existierender CBeteiligte Entity **Python Verantwortung** (Sync-Handler): - `syncing` - Lock während Sync-Prozess - `clean` - Nach erfolgreichem Sync - `failed` - Bei Sync-Fehlern mit Retry - `permanently_failed` - Nach zu vielen Retries - `conflict` - Bei erkannten Konflikten (optional) - `deleted_in_advoware` - Bei 404 von Advoware API --- ## Detaillierte Analyse ### ✅ Python-Managed Status (funktionieren perfekt) #### 1. `syncing` **Wann gesetzt**: Bei `acquire_sync_lock()` (Line 90) ```python await self.espocrm.update_entity('CBeteiligte', entity_id, { 'syncStatus': 'syncing' }) ``` **Sinnvoll**: ✅ Ja - verhindert parallele Syncs, UI-Feedback --- #### 2. `clean` **Wann gesetzt**: Bei `release_sync_lock()` nach erfolgreichem Sync ```python await sync_utils.release_sync_lock(entity_id, 'clean') ``` **Verwendungen**: - Nach CREATE: Line 223 (beteiligte_sync_event_step.py) - Nach espocrm_newer Sync: Line 336 - Nach advoware_newer Sync: Line 369 - Nach Konflikt-Auflösung: Line 423 + 643 (beteiligte_sync_utils.py) **Sinnvoll**: ✅ Ja - zeigt erfolgreichen Sync an --- #### 3. `failed` **Wann gesetzt**: Bei `release_sync_lock()` mit `increment_retry=True` ```python await sync_utils.release_sync_lock(entity_id, 'failed', str(e), increment_retry=True) ``` **Verwendungen**: - CREATE fehlgeschlagen: Line 235 - UPDATE fehlgeschlagen: Line 431 - Validation fehlgeschlagen: Lines 318, 358, 409 - Exception im Handler: Line 139 **Sinnvoll**: ✅ Ja - ermöglicht Retry-Logik --- #### 4. `permanently_failed` **Wann gesetzt**: Nach ≥ 5 Retries (Line 162, beteiligte_sync_utils.py) ```python if new_retry >= MAX_SYNC_RETRIES: update_data['syncStatus'] = 'permanently_failed' ``` **Auto-Reset**: Nach 24h durch Cron (Lines 64-85, beteiligte_sync_cron_step.py) ```python {'type': 'equals', 'attribute': 'syncStatus', 'value': 'permanently_failed'} # → Reset zu 'failed' nach 24h ``` **Sinnvoll**: ✅ Ja - verhindert endlose Retries, aber ermöglicht Recovery --- #### 5. `deleted_in_advoware` **Wann gesetzt**: Bei 404 von Advoware API (Line 530, beteiligte_sync_utils.py) ```python await self.espocrm.update_entity('CBeteiligte', entity_id, { 'syncStatus': 'deleted_in_advoware', 'advowareDeletedAt': now, 'syncErrorMessage': f"Beteiligter existiert nicht mehr in Advoware. {error_details}" }) ``` **Sinnvoll**: ✅ Ja - Soft-Delete Strategie, ermöglicht manuelle Überprüfung --- ### � EspoCRM-Managed Status (Webhook-Trigger + Cron-Fallback) Diese Status werden von **EspoCRM gesetzt** (nicht vom Python-Code): #### 6. `pending_sync` ✅ **Wann gesetzt**: Von **EspoCRM** bei CREATE neuer CBeteiligte Entity **Zweck**: - **Primary**: Webhook `vmh.beteiligte.create` triggert sofort - **Fallback**: Falls Webhook fehlschlägt, findet Cron diese Entities **Cron-Query** (Line 45, beteiligte_sync_cron_step.py): ```python {'type': 'equals', 'attribute': 'syncStatus', 'value': 'pending_sync'} ``` **Workflow**: ``` 1. User erstellt CBeteiligte in EspoCRM 2. EspoCRM setzt syncStatus = 'pending_sync' 3. PRIMARY: EspoCRM Webhook → vmh.beteiligte.create → Sofortiger Sync FALLBACK: Webhook failed → Cron (alle 15min) findet Entity via Status 4. Python Sync-Handler: pending_sync → syncing → clean/failed ``` **Sinnvoll**: ✅ Ja - Defense in Depth Design, garantiert Sync auch bei Webhook-Ausfall --- #### 7. `dirty` ✅ **Wann gesetzt**: Von **EspoCRM** bei UPDATE existierender CBeteiligte Entity **Zweck**: - **Primary**: Webhook `vmh.beteiligte.update` triggert sofort - **Fallback**: Falls Webhook fehlschlägt, findet Cron diese Entities **Cron-Query** (Line 46, beteiligte_sync_cron_step.py): ```python {'type': 'equals', 'attribute': 'syncStatus', 'value': 'dirty'} ``` **Workflow**: ``` 1. User ändert CBeteiligte in EspoCRM 2. EspoCRM setzt syncStatus = 'dirty' (nur wenn vorher 'clean') 3. PRIMARY: EspoCRM Webhook → vmh.beteiligte.update → Sofortiger Sync FALLBACK: Webhook failed → Cron (alle 15min) findet Entity via Status 4. Python Sync-Handler: dirty → syncing → clean/failed ``` **Sinnvoll**: ✅ Ja - Defense in Depth Design, garantiert Sync auch bei Webhook-Ausfall **Implementation in EspoCRM**: ```javascript // EspoCRM Hook: afterSave() in CBeteiligte entity.set('syncStatus', entity.isNew() ? 'pending_sync' : 'dirty'); ``` --- #### 8. `conflict` ⚠️ (Optional) **Wann gesetzt**: Aktuell **NIE** - Konflikte werden sofort auto-resolved **Aktuelles Verhalten**: ```python # Bei Konflikt-Erkennung: if comparison == 'conflict': # ... löse Konflikt (EspoCRM wins) await sync_utils.resolve_conflict_espocrm_wins(...) # Status geht direkt zu 'clean' ``` **Potential für Verbesserung**: ```python # Option: Intermediate 'conflict' Status für Admin-Review if comparison == 'conflict' and not AUTO_RESOLVE_CONFLICTS: await espocrm.update_entity('CBeteiligte', entity_id, { 'syncStatus': 'conflict', 'conflictDetails': conflict_details }) # Warte auf Admin-Aktion else: # Auto-Resolve wie aktuell ``` **Status**: ⚠️ Optional - Aktuelles Auto-Resolve funktioniert, aber `conflict` Status könnte UI-Visibility verbessern --- ## Cron-Job Queries Analyse **Datei**: `steps/vmh/beteiligte_sync_cron_step.py` ### Query 1: Normale Sync-Kandidaten ✅ ```python { 'type': 'or', 'value': [ {'type': 'equals', 'attribute': 'syncStatus', 'value': 'pending_sync'}, # ✅ Von EspoCRM gesetzt {'type': 'equals', 'attribute': 'syncStatus', 'value': 'dirty'}, # ✅ Von EspoCRM gesetzt {'type': 'equals', 'attribute': 'syncStatus', 'value': 'failed'}, # ✅ Von Python gesetzt ] } ``` **Status**: ✅ Funktioniert perfekt als Fallback-Mechanismus **Design-Vorteil**: - Webhook-Ausfall? Cron findet alle `pending_sync` und `dirty` Entities - Temporäre Fehler? Cron retried alle `failed` Entities mit Backoff - Robustes System mit Defense in Depth ### Query 2: Auto-Reset für permanently_failed ✅ ```python {'type': 'equals', 'attribute': 'syncStatus', 'value': 'permanently_failed'} # + syncAutoResetAt < now ``` **Status**: ✅ Funktioniert perfekt ### Query 3: Periodic Check für clean Entities ✅ ```python {'type': 'equals', 'attribute': 'syncStatus', 'value': 'clean'} # + advowareLastSync > 24 Stunden alt ``` **Status**: ✅ Funktioniert als zusätzliche Sicherheitsebene --- ## EspoCRM Integration Requirements Damit das System vollständig funktioniert, muss **EspoCRM** folgende Status setzen: ### 1. Bei Entity Creation (beforeSave/afterSave Hook) ```javascript // EspoCRM: CBeteiligte Entity Hook entity.set('syncStatus', 'pending_sync'); ``` ### 2. Bei Entity Update (beforeSave Hook) ```javascript // EspoCRM: CBeteiligte Entity Hook if (!entity.isNew() && entity.get('syncStatus') === 'clean') { // Prüfe ob sync-relevante Felder geändert wurden const syncRelevantFields = ['name', 'vorname', 'anrede', 'geburtsdatum', 'rechtsform', 'strasse', 'plz', 'ort', 'emailAddressData', 'phoneNumberData']; const hasChanges = syncRelevantFields.some(field => entity.isAttributeChanged(field)); if (hasChanges) { entity.set('syncStatus', 'dirty'); } } ``` ### 3. Entity Definition (entityDefs/CBeteiligte.json) ```json { "fields": { "syncStatus": { "type": "enum", "options": [ "pending_sync", "dirty", "syncing", "clean", "failed", "permanently_failed", "conflict", "deleted_in_advoware" ], "default": "pending_sync", "required": true, "readOnly": true } } } ``` --- ## System-Architektur: Vollständiger Flow ### Szenario 1: CREATE (Happy Path mit Webhook) ``` 1. User erstellt CBeteiligte in EspoCRM 2. EspoCRM Hook setzt syncStatus = 'pending_sync' 3. EspoCRM Webhook triggert vmh.beteiligte.create Event 4. Python Event-Handler: - acquire_lock() → syncStatus = 'syncing' - handle_create() → POST zu Advoware - release_lock() → syncStatus = 'clean' 5. ✅ Erfolgreich synchronisiert ``` ### Szenario 2: CREATE (Webhook failed → Cron Fallback) ``` 1. User erstellt CBeteiligte in EspoCRM 2. EspoCRM Hook setzt syncStatus = 'pending_sync' 3. ❌ Webhook Service down/failed 4. 15 Minuten später: Cron läuft 5. Cron Query findet Entity via syncStatus = 'pending_sync' 6. Cron emittiert vmh.beteiligte.sync_check Event 7. Python Event-Handler wie in Szenario 1 8. ✅ Erfolgreich synchronisiert (mit Verzögerung) ``` ### Szenario 3: UPDATE (Happy Path mit Webhook) ``` 1. User ändert CBeteiligte in EspoCRM 2. EspoCRM Hook setzt syncStatus = 'dirty' (war vorher 'clean') 3. EspoCRM Webhook triggert vmh.beteiligte.update Event 4. Python Event-Handler: - acquire_lock() → syncStatus = 'syncing' - handle_update() → Sync-Logik - release_lock() → syncStatus = 'clean' 5. ✅ Erfolgreich synchronisiert ``` ### Szenario 4: Sync-Fehler mit Retry ``` 1-3. Wie Szenario 1/3 4. Python Event-Handler: - acquire_lock() → syncStatus = 'syncing' - handle_xxx() → ❌ Exception - release_lock(increment_retry=True) → syncStatus = 'failed', syncNextRetry = now + backoff 5. Cron findet Entity via syncStatus = 'failed' 6. Prüft syncNextRetry → noch nicht erreicht → skip 7. Nach Backoff-Zeit: Retry 8. Erfolgreich → syncStatus = 'clean' ODER nach 5 Retries → syncStatus = 'permanently_failed' ``` --- ## Empfehlungen ### ✅ Status-Design ist korrekt Das aktuelle Design mit 8 Status ist **optimal** für: - Defense in Depth (Webhook + Cron Fallback) - Robustheit bei Webhook-Ausfall - Retry-Mechanismus mit Exponential Backoff - Soft-Delete Strategie - UI-Visibility ### 🔵 EspoCRM Implementation erforderlich **CRITICAL**: EspoCRM muss folgende Status setzen: 1. ✅ `pending_sync` bei CREATE 2. ✅ `dirty` bei UPDATE (nur wenn vorher `clean`) 3. ✅ Default-Wert in Entity Definition **Implementation**: EspoCRM Hooks in CBeteiligte Entity ### 🟡 Optional: Conflict Status **Current**: Auto-Resolve funktioniert **Enhancement**: Intermediate `conflict` Status für UI-Visibility und Admin-Review --- ## Zusammenfassung ### Status-Verteilung **EspoCRM Verantwortung** (2 Status): - ✅ `pending_sync` - Bei CREATE - ✅ `dirty` - Bei UPDATE **Python Verantwortung** (6 Status): - ✅ `syncing` - Lock während Sync - ✅ `clean` - Erfolgreich gesynct - ✅ `failed` - Retry nötig - ✅ `permanently_failed` - Max retries erreicht - ✅ `deleted_in_advoware` - 404 von Advoware - ⚠️ `conflict` - Optional für UI-Visibility ### System-Qualität **Architektur**: ⭐⭐⭐⭐⭐ (5/5) - Defense in Depth Design **Robustheit**: ⭐⭐⭐⭐⭐ (5/5) - Funktioniert auch bei Webhook-Ausfall **Status-Design**: ⭐⭐⭐⭐⭐ (5/5) - Alle Status sinnvoll und notwendig **Einzige Requirement**: EspoCRM muss `pending_sync` und `dirty` setzen --- **Review erstellt von**: GitHub Copilot **Review-Datum**: 8. Februar 2026 (Updated) **Status**: ✅ Design validiert, EspoCRM Integration dokumentiert