diff --git a/bitbylaw/docs/SYNC_STATUS_ANALYSIS.md b/bitbylaw/docs/SYNC_STATUS_ANALYSIS.md new file mode 100644 index 00000000..b73bcb4e --- /dev/null +++ b/bitbylaw/docs/SYNC_STATUS_ANALYSIS.md @@ -0,0 +1,418 @@ +# 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