feat: Implement bidirectional synchronization utilities for Advoware and EspoCRM communications

- Added KommunikationSyncManager class to handle synchronization logic.
- Implemented methods for loading data, computing diffs, and applying changes between Advoware and EspoCRM.
- Introduced 3-way diffing mechanism to intelligently resolve conflicts.
- Added helper methods for creating empty slots and detecting changes in communications.
- Enhanced logging for better traceability during synchronization processes.
This commit is contained in:
2026-02-08 19:53:40 +00:00
parent da9a962858
commit ebbbf419ee
23 changed files with 7626 additions and 13 deletions

View File

@@ -0,0 +1,319 @@
# Kommunikation Sync Implementation
## Overview
Bidirektionale Synchronisation von Email- und Telefon-Daten zwischen Advoware und EspoCRM.
## Architektur-Übersicht
```
┌─────────────────────────────────────────────────────────────────┐
│ Advoware ↔ EspoCRM Sync │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ADVOWARE ESPOCRM │
│ ───────────────── ────────────────── │
│ Beteiligte CBeteiligte │
│ └─ kommunikation[] ├─ emailAddressData[] │
│ ├─ id (unique int) │ └─ emailAddress │
│ ├─ rowId (string) │ lower, primary │
│ ├─ tlf (value) │ │
│ ├─ bemerkung (marker!) └─ phoneNumberData[] │
│ ├─ kommKz (1-12) └─ phoneNumber │
│ └─ online (bool) type, primary │
│ │
│ MATCHING: Hash in bemerkung-Marker │
│ [ESPOCRM:hash:kommKz] User text │
└─────────────────────────────────────────────────────────────────┘
```
## Core Features
### 1. Base64-basiertes Matching ✅ IMPLEMENTIERT
- **Problem**: EspoCRM Arrays haben keine IDs
- **Lösung**: Base64-kodierter Wert in Advoware bemerkung
- **Format**: `[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Geschäftlich`
- **Vorteil**: Bidirektional! Marker enthält den tatsächlichen Wert (dekodierbar)
**Warum Base64 statt Hash?**
```python
# Hash-Problem (alt): Nicht rückrechenbar
old_hash = hash("old@example.com") # abc12345
# Bei Wert-Änderung in Advoware: Kein Match möglich! ❌
# Base64-Lösung (neu): Bidirektional
encoded = base64("old@example.com") # b2xkQGV4YW1wbGUuY29t
decoded = decode(encoded) # "old@example.com" ✅
# Kann dekodieren → Match in EspoCRM finden!
```
### 2. 4-Stufen Typ-Erkennung
```python
1. Aus Marker: [ESPOCRM:hash:3] kommKz=3 (Mobil)
2. Aus Top-Level: beteiligte.mobil kommKz=3
3. Aus Pattern: '@' in value kommKz=4 (Email)
4. Default: Fallback kommKz=1 oder 4
```
### 3. Empty Slot System
- **Problem**: DELETE ist 403 Forbidden in Advoware
- **Lösung**: Leere Slots mit `[ESPOCRM-SLOT:kommKz]`
- **Wiederverwendung**: Neue Einträge reuse leere Slots
### 4. Asymmetrischer Sync
**Problem**: Hash-basiertes Matching funktioniert NICHT bidirektional
- Wenn Wert in Advoware ändert: Hash ändert sich → Kein Match in EspoCRM möglich
**Lösung**: Verschiedene Strategien je Richtung
| Richtung | Methode | Grund |
|----------|---------|-------|
| **Advoware → EspoCRM** | FULL SYNC (kompletter Overwrite) | Kein stabiles Matching möglich |
| **EspoCRM → Advoware** | INCREMENTAL SYNC (Hash-basiert) | EspoCRM-Wert bekannt → Hash berechenbar |
**Ablauf Advoware → EspoCRM (FULL SYNC)**:
```python
1. Sammle ALLE Kommunikationen (ohne Empty Slots)
2. Setze/Update Marker für Rück-Sync
3. Ersetze KOMPLETTE emailAddressData[] und phoneNumberData[]
```
**Ablauf EspoCRM → Advoware (INCREMENTAL)**:
```python
1. Baue Hash-Maps von beiden Seiten
2. Vergleiche: Deleted, Changed, New
3. Apply Changes (Empty Slots, Updates, Creates)
```
## Module Structure
```
services/
├── kommunikation_mapper.py # Datentyp-Mapping & Marker-Logik
├── advoware_service.py # Advoware API-Wrapper
└── kommunikation_sync_utils.py # Sync-Manager (bidirectional)
```
## Usage Example
```python
from services.advoware_service import AdvowareService
from services.espocrm import EspoCrmService
from services.kommunikation_sync_utils import KommunikationSyncManager
# Initialize
advo = AdvowareService()
espo = EspoCrmService()
sync_manager = KommunikationSyncManager(advo, espo)
# Bidirectional Sync
result = sync_manager.sync_bidirectional(
beteiligte_id='espocrm-bet-id',
betnr=12345,
direction='both' # 'both', 'to_espocrm', 'to_advoware'
)
print(result)
# {
# 'advoware_to_espocrm': {
# 'emails_synced': 3,
# 'phones_synced': 2,
# 'errors': []
# },
# 'espocrm_to_advoware': {
# 'created': 1,
# 'updated': 2,
# 'deleted': 0,
# 'errors': []
# }
# }
```
## Field Mapping
### kommKz Enum (Advoware)
| kommKz | Name | EspoCRM Target | EspoCRM Type |
|--------|------|----------------|--------------|
| 1 | TelGesch | phoneNumberData | Office |
| 2 | FaxGesch | phoneNumberData | Fax |
| 3 | Mobil | phoneNumberData | Mobile |
| 4 | MailGesch | emailAddressData | - |
| 5 | Internet | *(skipped)* | - |
| 6 | TelPrivat | phoneNumberData | Home |
| 7 | FaxPrivat | phoneNumberData | Fax |
| 8 | MailPrivat | emailAddressData | - |
| 9 | AutoTelefon | phoneNumberData | Mobile |
| 10 | Sonstige | phoneNumberData | Other |
| 11 | EPost | emailAddressData | - |
| 12 | Bea | emailAddressData | - |
**Note**: Internet (kommKz=5) wird nicht synchronisiert (unklar ob Email/Phone).
## Sync Scenarios
### Scenario 1: Delete in EspoCRM
```
EspoCRM: max@example.com gelöscht
Advoware: [ESPOCRM:abc:4] max@example.com
→ UPDATE zu Empty Slot:
tlf: ''
bemerkung: [ESPOCRM-SLOT:4]
online: False
```
### Scenario 2: Change in EspoCRM
```
EspoCRM: max@old.com → max@new.com
Advoware: [ESPOCRM:oldhash:4] max@old.com
→ UPDATE with new hash:
tlf: 'max@new.com'
bemerkung: [ESPOCRM:newhash:4] Geschäftlich
online: True
```
### Scenario 3: New in EspoCRM
```
EspoCRM: Neue Email new@example.com
→ Suche Empty Slot (kommKz=4)
IF found: REUSE (UPDATE)
ELSE: CREATE new
```
### Scenario 4: New in Advoware
```
Advoware: Neue Kommunikation (kein Marker)
→ Typ-Erkennung via Top-Level/Pattern
→ Sync zu EspoCRM
→ Marker in Advoware setzen
```
## API Limitations
### Advoware API v1
-**POST**: /api/v1/advonet/Beteiligte/{betnr}/Kommunikationen
- Required: tlf, kommKz
- Optional: bemerkung, online
-**PUT**: /api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{id}
- Writable: tlf, bemerkung, online
- **READ-ONLY**: kommKz (cannot change type!)
-**DELETE**: 403 Forbidden
- Use Empty Slots instead
- ⚠️ **BUG**: kommKz always returns 0 in GET
- Use Top-Level fields + Pattern detection
### EspoCRM
-**emailAddressData**: Array ohne IDs
-**phoneNumberData**: Array ohne IDs
-**Kein CKommunikation Entity**: Arrays nur in CBeteiligte
## Testing
Run all tests:
```bash
cd /opt/motia-app/bitbylaw
python3 scripts/test_kommunikation_sync_implementation.py
```
**Test Coverage**:
- ✅ Hash-Berechnung und Konsistenz
- ✅ Marker-Parsing (Standard + Slot)
- ✅ Marker-Erstellung
- ✅ 4-Stufen Typ-Erkennung (alle Tiers)
- ✅ Typ-Klassifizierung (Email vs Phone)
- ✅ Integration Szenario
- ✅ Top-Level Feld Priorität
## Change Detection
### Advoware Webhook
```python
from services.kommunikation_sync_utils import detect_kommunikation_changes
if detect_kommunikation_changes(old_bet, new_bet):
# rowId changed → Sync needed
sync_manager.sync_bidirectional(bet_id, betnr, direction='to_espocrm')
```
### EspoCRM Webhook
```python
from services.kommunikation_sync_utils import detect_espocrm_kommunikation_changes
if detect_espocrm_kommunikation_changes(old_data, new_data):
# Array changed → Sync needed
sync_manager.sync_bidirectional(bet_id, betnr, direction='to_advoware')
```
## Known Limitations
1. **FULL SYNC von Advoware → EspoCRM**:
- Arrays werden komplett überschrieben (kein Merge)
- Grund: Hash-basiertes Matching funktioniert nicht bei Wert-Änderungen in Advoware
- Risiko minimal: EspoCRM-Arrays haben keine Relationen
2. **Empty Slots Accumulation**:
- Gelöschte Einträge werden zu leeren Slots
- Werden wiederverwendet, aber akkumulieren
- TODO: Periodic cleanup job
3. **Partial Type Loss**:
- Advoware-Kommunikationen ohne Top-Level Match verlieren Feintyp
- Fallback: @ → Email (4), sonst Phone (1)
4. **kommKz READ-ONLY**:
- Typ kann nach Erstellung nicht geändert werden
- Workaround: DELETE + CREATE (manuell)
5. **Marker sichtbar**:
- `[ESPOCRM:...]` ist in Advoware UI sichtbar
- User kann Text dahinter hinzufügen
## Documentation
- **Vollständige Analyse**: [docs/KOMMUNIKATION_SYNC_ANALYSE.md](../docs/KOMMUNIKATION_SYNC_ANALYSE.md)
- **API Tests**: [scripts/test_kommunikation_api.py](test_kommunikation_api.py)
- **Implementation Tests**: [scripts/test_kommunikation_sync_implementation.py](test_kommunikation_sync_implementation.py)
## Implementation Status
**COMPLETE**
- [x] Marker-System (Hash + kommKz)
- [x] 4-Stufen Typ-Erkennung
- [x] Empty Slot System
- [x] Bidirektionale Sync-Logik
- [x] Advoware Service Wrapper
- [x] Change Detection
- [x] Test Suite
- [x] Documentation
## Next Steps
1. **Integration in Webhook System**
- Add kommunikation change detection to beteiligte webhooks
- Wire up sync calls
2. **Monitoring**
- Add metrics for sync operations
- Track empty slot accumulation
3. **Maintenance**
- Implement periodic cleanup job for old empty slots
- Add notification for type-change scenarios
4. **Testing**
- End-to-end tests with real Advoware/EspoCRM data
- Load testing for large kommunikation arrays
---
**Last Updated**: 2024-01-26
**Status**: ✅ Implementation Complete - Ready for Integration