feat: Add Kommunikation-Sync documentation for bidirectional synchronization between EspoCRM and Advoware

This commit is contained in:
2026-02-08 19:58:43 +00:00
parent ebbbf419ee
commit bfe2f4f7e3
2 changed files with 854 additions and 25 deletions

View File

@@ -110,13 +110,15 @@ Acquire Lock (Redis)
GET /api/v1/advonet/Beteiligte/{betnr}
Timestamp-Vergleich:
- espocrm_newer → Update Advoware (PUT)
- advoware_newer → Update EspoCRM (PATCH)
- conflict → EspoCRM wins (PUT) + Notification
- no_change → Skip
Timestamp-Vergleich (rowId + modifiedAt vs geaendertAm):
- no_change → Nur Kommunikation sync (direction=both)
- espocrm_newer → Update Advoware (PUT) + Kommunikation sync (direction=both)
- advoware_newer → Update EspoCRM (PATCH) + Kommunikation sync (direction=both)
- conflict → EspoCRM wins (PUT) + Notification + Kommunikation sync (direction=to_advoware ONLY!)
Release Lock
Kommunikation Sync (Hash-basiert, siehe unten)
Release Lock (NACH Kommunikation-Sync!)
```
### 3. Cron Check
@@ -180,11 +182,63 @@ results = await asyncio.gather(*tasks, return_exceptions=True)
## Kommunikation-Sync Integration
### Base64-Marker Strategie ✅
**WICHTIG**: Kommunikation-Sync läuft **IMMER** nach Stammdaten-Sync (auch bei `no_change`)!
Die Kommunikation-Synchronisation (Telefon, Email) ist in den Beteiligte-Sync integriert.
### Hash-basierte Änderungserkennung ✅
**Marker-Format**:
Die Kommunikation-Synchronisation verwendet **MD5-Hash** der `kommunikation` rowIds aus Advoware:
- **Hash-Berechnung**: MD5 von sortierten rowIds (erste 16 Zeichen)
- **Speicherort**: `kommunikationHash` in EspoCRM CBeteiligte
- **Vorteil**: Erkennt Kommunikations-Änderungen ohne Beteiligte-rowId-Änderung
**Problem gelöst**: Beteiligte-rowId ändert sich **NICHT**, wenn nur Kommunikation geändert wird!
### 3-Way Diffing mit Konflikt-Erkennung
```python
# Timestamp-basiert für EspoCRM
espo_changed = espo_bet.modifiedAt > espo_bet.advowareLastSync
# Hash-basiert für Advoware
stored_hash = espo_bet.kommunikationHash # z.B. "a3f5d2e8b1c4f6a9"
current_hash = MD5(sorted(komm.rowId for komm in advo_kommunikationen))[:16]
advo_changed = stored_hash != current_hash
# Konflikt-Erkennung
if espo_changed AND advo_changed:
espo_wins = True # EspoCRM gewinnt immer!
```
### Konflikt-Behandlung: EspoCRM Wins
**Bei Konflikt** (beide Seiten geändert):
1. **Stammdaten**: EspoCRM → Advoware (PUT)
2. **Kommunikation**: `direction='to_advoware'` (NUR EspoCRM→Advoware, blockiert Advoware→EspoCRM)
3. **Notification**: In-App Benachrichtigung
4. **Hash-Update**: Neuer Hash wird gespeichert
**Ohne Konflikt**:
- **Stammdaten**: Je nach Timestamp-Vergleich
- **Kommunikation**: `direction='both'` (bidirektional)
### 6 Sync-Varianten (Var1-6)
**Var1**: Neu in EspoCRM → CREATE in Advoware
**Var2**: Gelöscht in EspoCRM → DELETE in Advoware (Empty Slot)
**Var3**: Gelöscht in Advoware → DELETE in EspoCRM
**Var4**: Neu in Advoware → CREATE in EspoCRM
**Var5**: Geändert in EspoCRM → UPDATE in Advoware
**Var6**: Geändert in Advoware → UPDATE in EspoCRM
### Base64-Marker Strategie
```
[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Geschäftlich
[ESPOCRM-SLOT:4] # Leerer Slot nach Löschung
```
### Base64-Marker Strategie
**Marker-Format** im Advoware `bemerkung` Feld:
```
[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Geschäftlich
[ESPOCRM-SLOT:4] # Leerer Slot nach Löschung
@@ -201,28 +255,90 @@ Die Kommunikation-Synchronisation (Telefon, Email) ist in den Beteiligte-Sync in
# Update: EspoCRM-Eintrag + Marker mit neuem Base64-Wert
```
**Async/Await Architektur** ⚡:
- Alle Sync-Methoden sind **async** für Non-Blocking I/O
- AdvowareService: Native async (kein `asyncio.run()` mehr)
- KommunikationSyncManager: Vollständig async mit proper await
- Integration im Webhook-Handler: Seamless async/await flow
### 4-Stufen kommKz-Erkennung (Type Detection)
**4-Stufen Typ-Erkennung**:
1. **Marker** (höchste Priorität) → `[ESPOCRM:...:3]` = kommKz 3
2. **Top-Level Felder** → `beteiligte.mobil` = kommKz 3
3. **Wert-Pattern** → `@` in Wert = Email (kommKz 4)
4. **Default** → Fallback (TelGesch=1, MailGesch=4)
**Problem**: Advoware `kommKz` ist via GET immer 0, via PUT read-only!
**Bidirektionale Sync**:
- **Advoware → EspoCRM**: Komplett (inkl. Marker-Update bei Wert-Änderung)
- **EspoCRM → Advoware**: Vollständig (CREATE/UPDATE/DELETE via Slots)
- **Slot-Wiederverwendung**: Gelöschte Einträge werden als `[ESPOCRM-SLOT:kommKz]` markiert
**Lösung - Prioritäts-Kaskade**:
1. **Marker** (höchste Priorität) → `[ESPOCRM:...:3]` = kommKz 3 (Mobil)
2. **EspoCRM Type** (bei EspoCRM→Advoware) → `type: 'Mobile'` = kommKz 3
3. **Top-Level Felder** → `beteiligte.mobil` = kommKz 3
4. **Wert-Pattern** → `@` in Wert = Email (kommKz 4)
5. **Default** → Fallback (TelGesch=1, MailGesch=4)
**Mapping EspoCRM phoneNumberData.type → kommKz**:
```python
PHONE_TYPE_TO_KOMMKZ = {
'Office': 1, # TelGesch
'Fax': 2, # FaxGesch
'Mobile': 3, # Mobil
'Home': 6, # TelPrivat
'Other': 10 # Sonstige
}
```
### Slot-Wiederverwendung (Empty Slots)
**Problem**: Advoware DELETE gibt 403 Forbidden!
**Lösung**: Empty Slots mit Marker
```python
# Gelöscht in EspoCRM → Create Empty Slot in Advoware
{
"tlf": "",
"bemerkung": "[ESPOCRM-SLOT:4]", # kommKz=4 (Email)
"kommKz": 4,
"online": True
}
```
**Wiederverwendung**:
- Neue Einträge prüfen zuerst Empty Slots mit passendem kommKz
- UPDATE statt CREATE spart API-Calls und IDs
### Lock-Management mit Redis
**WICHTIG**: Lock wird erst NACH Kommunikation-Sync freigegeben!
```python
# Pattern in allen 4 Szenarien:
await sync_utils.acquire_sync_lock(entity_id)
try:
# 1. Stammdaten sync
# 2. Kommunikation sync (run_kommunikation_sync helper)
# 3. Lock release
await sync_utils.release_sync_lock(entity_id, 'clean')
finally:
# Failsafe: Lock wird auch bei Exception released
pass
```
**Vorher (BUG)**: Lock wurde teilweise VOR Kommunikation-Sync released!
**Jetzt**: Konsistentes Pattern - Lock schützt gesamte Operation
### Implementation Details
**Implementation**:
- [kommunikation_mapper.py](../services/kommunikation_mapper.py) - Base64 encoding/decoding
- [kommunikation_sync_utils.py](../services/kommunikation_sync_utils.py) - Sync-Manager
- [kommunikation_mapper.py](../services/kommunikation_mapper.py) - Base64 encoding/decoding, kommKz detection
- [kommunikation_sync_utils.py](../services/kommunikation_sync_utils.py) - Sync-Manager mit 3-way diffing
- [beteiligte_sync_event_step.py](../steps/vmh/beteiligte_sync_event_step.py) - Event handler mit helper function
- Tests: [test_kommunikation_sync_implementation.py](../scripts/test_kommunikation_sync_implementation.py)
**Helper Function** (DRY-Prinzip):
```python
async def run_kommunikation_sync(entity_id, betnr, komm_sync, context, direction='both'):
"""Führt Kommunikation-Sync aus mit Error-Handling und Logging"""
context.logger.info(f"📞 Starte Kommunikation-Sync (direction={direction})...")
komm_result = await komm_sync.sync_bidirectional(entity_id, betnr, direction=direction)
return komm_result
```
**Verwendet in**:
- no_change: `direction='both'`
- espocrm_newer: `direction='both'`
- advoware_newer: `direction='both'`
- **conflict**: `direction='to_advoware'` ← NUR EspoCRM→Advoware!
## Performance
| Operation | API Calls | Latency |
@@ -357,6 +473,8 @@ Custom fields für Sync-Management:
- `syncStatus` (enum) - Sync-Status
- `advowareLastSync` (datetime) - Letzter erfolgreicher Sync
- `advowareDeletedAt` (datetime) - Soft-Delete timestamp
- `advowareRowId` (varchar, 50) - Cached Advoware rowId für Change Detection
- **`kommunikationHash` (varchar, 16)** - MD5-Hash der Kommunikation rowIds (erste 16 Zeichen)
- `syncErrorMessage` (text, 2000 chars) - Letzte Fehlermeldung
- `syncRetryCount` (int) - Anzahl fehlgeschlagener Versuche