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:
319
bitbylaw/services/KOMMUNIKATION_SYNC_README.md
Normal file
319
bitbylaw/services/KOMMUNIKATION_SYNC_README.md
Normal 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
|
||||
Reference in New Issue
Block a user