- 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.
320 lines
10 KiB
Markdown
320 lines
10 KiB
Markdown
# 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
|