feat(docs): Add SYNC_STATUS_ANALYSIS documentation for syncStatus values and responsibilities in EspoCRM
This commit is contained in:
418
bitbylaw/docs/SYNC_STATUS_ANALYSIS.md
Normal file
418
bitbylaw/docs/SYNC_STATUS_ANALYSIS.md
Normal file
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### <20> 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
|
||||||
Reference in New Issue
Block a user