Files
motia/bitbylaw/docs/BETEILIGTE_SYNC.md
bitbylaw ebbbf419ee 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.
2026-02-08 19:53:40 +00:00

405 lines
12 KiB
Markdown

# Beteiligte Sync - Bidirektionale Synchronisation EspoCRM ↔ Advoware
## Übersicht
Bidirektionale Synchronisation der **Stammdaten** von Beteiligten zwischen EspoCRM (CBeteiligte) und Advoware (Beteiligte).
**Scope**: Nur Stammdaten (Name, Rechtsform, Geburtsdatum, Anrede, Handelsregister)
**Out of Scope**: Kontaktdaten (Telefon, Email, Fax, Bankverbindungen) → separate Endpoints
## Architektur
### Event-Driven Architecture
```
┌─────────────┐
│ EspoCRM │ Webhook → vmh.beteiligte.{create,update,delete}
│ CBeteiligte │ ↓
└─────────────┘ ┌────────────────────┐
│ Event Handler │
┌─────────────┐ │ (sync_event_step) │
│ Cron │ ───→ │ │
│ (15 min) │ sync_ │ - Lock (Redis) │
└─────────────┘ check │ - Timestamp Check │
│ - Merge & Sync │
└────────┬───────────┘
┌────────────────────┐
│ Advoware API │
│ /Beteiligte │
└────────────────────┘
```
### Komponenten
1. **Event Handler** ([beteiligte_sync_event_step.py](../steps/vmh/beteiligte_sync_event_step.py))
- Subscribes: `vmh.beteiligte.{create,update,delete,sync_check}`
- Verarbeitet Sync-Events
- Verwendet Redis distributed lock
2. **Cron Job** ([beteiligte_sync_cron_step.py](../steps/vmh/beteiligte_sync_cron_step.py))
- Läuft alle 15 Minuten
- Findet Entities mit Sync-Bedarf
- Emittiert `sync_check` Events
3. **Sync Utils** ([beteiligte_sync_utils.py](../services/beteiligte_sync_utils.py))
- Lock-Management (Redis distributed lock)
- Timestamp-Vergleich
- Merge-Utility für Advoware PUT
- Notifications
4. **Mapper** ([espocrm_mapper.py](../services/espocrm_mapper.py))
- `map_cbeteiligte_to_advoware()` - EspoCRM → Advoware
- `map_advoware_to_cbeteiligte()` - Advoware → EspoCRM
- Nur Stammdaten, keine Kontaktdaten
5. **APIs**
- [espocrm.py](../services/espocrm.py) - EspoCRM API Client
- [advoware.py](../services/advoware.py) - Advoware API Client
## Sync-Strategie
### State Management
- **Sync-Status in EspoCRM** (nicht PostgreSQL)
- **Field**: `syncStatus` (enum mit 7 Werten)
- **Lock**: Redis distributed lock (5 min TTL)
### Konfliktauflösung
- **Policy**: EspoCRM wins
- **Detection**: Timestamp-Vergleich (`modifiedAt` vs `geaendertAm`)
- **Notification**: In-App Notification in EspoCRM
### Sync-Status Values
```typescript
enum SyncStatus {
clean // ✅ Synced, keine Änderungen
dirty // 📝 Lokale Änderungen, noch nicht synced
pending_sync // ⏳ Wartet auf ersten Sync
syncing // 🔄 Sync läuft gerade (Lock)
failed // ❌ Sync fehlgeschlagen (retry möglich)
conflict // ⚠️ Konflikt erkannt
permanently_failed // 💀 Max retries erreicht (5x)
}
```
## Datenfluss
### 1. Create (Neu in EspoCRM)
```
EspoCRM (neu) → Webhook → Event Handler
Acquire Lock (Redis)
Map EspoCRM → Advoware
POST /api/v1/advonet/Beteiligte
Response: {betNr: 12345}
Update EspoCRM: betnr=12345, syncStatus=clean
Release Lock
```
### 2. Update (Änderung in EspoCRM)
```
EspoCRM (geändert) → Webhook → Event Handler
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
Release Lock
```
### 3. Cron Check
```
Cron (alle 15 min)
Query EspoCRM:
- syncStatus IN (pending_sync, dirty, failed)
- OR (clean AND advowareLastSync > 24h)
Batch emit: vmh.beteiligte.sync_check events
Event Handler (siehe Update)
```
## Optimierungen
### 1. Redis Distributed Lock (Atomicity)
```python
lock_key = f"sync_lock:cbeteiligte:{entity_id}"
acquired = redis.set(lock_key, "locked", nx=True, ex=300)
```
- ✅ Verhindert Race Conditions
- ✅ TTL verhindert Deadlocks (5 min)
### 2. Combined API Calls (Performance)
```python
await sync_utils.release_sync_lock(
entity_id,
'clean',
extra_fields={'betnr': new_betnr} # ← kombiniert 2 calls in 1
)
```
- ✅ 33% weniger API Requests
### 3. Merge Utility (Code Quality)
```python
merged = sync_utils.merge_for_advoware_put(advo_entity, espo_entity, mapper)
```
- ✅ Keine Code-Duplikation
- ✅ Konsistentes Logging
- ✅ Wiederverwendbar
### 4. Max Retry Limit (Robustheit)
```python
MAX_SYNC_RETRIES = 5
if retry_count >= 5:
status = 'permanently_failed'
send_notification("Max retries erreicht")
```
- ✅ Verhindert infinite loops
- ✅ User wird benachrichtigt
### 5. Batch Processing (Scalability)
```python
tasks = [context.emit(...) for entity_id in entity_ids]
results = await asyncio.gather(*tasks, return_exceptions=True)
```
- ✅ 90% schneller bei 100 Entities
## Kommunikation-Sync Integration
### Base64-Marker Strategie ✅
Die Kommunikation-Synchronisation (Telefon, Email) ist in den Beteiligte-Sync integriert.
**Marker-Format**:
```
[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Geschäftlich
[ESPOCRM-SLOT:4] # Leerer Slot nach Löschung
```
**Base64-Encoding statt Hash**:
- **Vorteil**: Bidirektional! Marker enthält den **tatsächlichen Wert** (Base64-kodiert)
- **Matching**: Selbst wenn Wert in Advoware ändert, kann alter Wert aus Marker dekodiert werden
- **Beispiel**:
```python
# Advoware: old@example.com → new@example.com
# Alter Marker: [ESPOCRM:b2xkQGV4YW1wbGUuY29t:4]
# Sync dekodiert: "old@example.com" → Findet Match in EspoCRM ✅
# 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 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)
**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
**Implementation**:
- [kommunikation_mapper.py](../services/kommunikation_mapper.py) - Base64 encoding/decoding
- [kommunikation_sync_utils.py](../services/kommunikation_sync_utils.py) - Sync-Manager
- Tests: [test_kommunikation_sync_implementation.py](../scripts/test_kommunikation_sync_implementation.py)
## Performance
| Operation | API Calls | Latency |
|-----------|-----------|---------|
| CREATE | 2 | ~200ms |
| UPDATE (initial) | 2 | ~250ms |
| UPDATE (normal) | 2 | ~250ms |
| Cron (100 entities) | 200 | ~1s (parallel) |
## Monitoring
### Sync-Status Tracking
```sql
-- In EspoCRM
SELECT syncStatus, COUNT(*)
FROM c_beteiligte
GROUP BY syncStatus;
```
### Failed Syncs
```sql
-- Entities mit Sync-Problemen
SELECT id, name, syncStatus, syncErrorMessage, syncRetryCount
FROM c_beteiligte
WHERE syncStatus IN ('failed', 'permanently_failed')
ORDER BY syncRetryCount DESC;
```
## Fehlerbehandlung
### Retriable Errors
- Netzwerk-Timeout
- 500 Internal Server Error
- 503 Service Unavailable
→ Status: `failed`, retry beim nächsten Cron
### Non-Retriable Errors
- 400 Bad Request (invalid data)
- 404 Not Found (entity deleted)
- 401 Unauthorized (auth error)
→ Status: `failed`, keine automatischen Retries
### Max Retries Exceeded
- Nach 5 Versuchen: `permanently_failed`
- User erhält In-App Notification
- Manuelle Prüfung erforderlich
## Testing
### Unit Tests
```bash
cd /opt/motia-app/bitbylaw
source python_modules/bin/activate
python scripts/test_beteiligte_sync.py
```
### Manual Test
```python
# Test single entity sync
event_data = {
'entity_id': '68e3e7eab49f09adb',
'action': 'sync_check',
'source': 'manual_test'
}
await beteiligte_sync_event_step.handler(event_data, context)
```
## Entity Mapping
### EspoCRM CBeteiligte → Advoware Beteiligte
| EspoCRM Field | Advoware Field | Type | Notes |
|---------------|----------------|------|-------|
| `lastName` | `name` | string | Bei Person |
| `firstName` | `vorname` | string | Bei Person |
| `firmenname` | `name` | string | Bei Firma |
| `rechtsform` | `rechtsform` | string | Person/Firma |
| `salutationName` | `anrede` | string | Herr/Frau |
| `dateOfBirth` | `geburtsdatum` | date | Nur Person |
| `handelsregisterNummer` | `handelsRegisterNummer` | string | Nur Firma |
| `betnr` | `betNr` | int | Foreign Key |
**Nicht gemapped**: Telefon, Email, Fax, Bankverbindungen (→ separate Endpoints)
## Troubleshooting
### Sync bleibt bei "syncing" hängen
**Problem**: Redis lock expired, aber syncStatus nicht zurückgesetzt
**Lösung**:
```python
# Lock ist automatisch nach 5 min weg (TTL)
# Manuelles zurücksetzen:
await espocrm.update_entity('CBeteiligte', entity_id, {'syncStatus': 'dirty'})
```
### "Max retries exceeded"
**Problem**: Entity ist `permanently_failed`
**Lösung**:
1. Prüfe `syncErrorMessage` für Details
2. Behebe das Problem (z.B. invalide Daten)
3. Reset: `syncStatus='dirty', syncRetryCount=0`
### Race Condition / Parallele Syncs
**Problem**: Zwei Syncs gleichzeitig (sollte nicht passieren)
**Lösung**: Redis lock verhindert das automatisch
## Configuration
### Environment Variables
```bash
# EspoCRM
ESPOCRM_API_BASE_URL=https://crm.bitbylaw.com/api/v1
ESPOCRM_MARVIN_API_KEY=e53def10eea27b92a6cd00f40a3e09a4
# Advoware
ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/
ADVOWARE_PRODUCT_ID=...
ADVOWARE_APP_ID=...
ADVOWARE_API_KEY=...
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB_ADVOWARE_CACHE=1
```
### EspoCRM Entity Fields
Custom fields für Sync-Management:
- `betnr` (int, unique) - Foreign Key zu Advoware
- `syncStatus` (enum) - Sync-Status
- `advowareLastSync` (datetime) - Letzter erfolgreicher Sync
- `advowareDeletedAt` (datetime) - Soft-Delete timestamp
- `syncErrorMessage` (text, 2000 chars) - Letzte Fehlermeldung
- `syncRetryCount` (int) - Anzahl fehlgeschlagener Versuche
## Deployment
### 1. Deploy Code
```bash
cd /opt/motia-app/bitbylaw
git pull
source python_modules/bin/activate
pip install -r requirements.txt
```
### 2. Restart Motia
```bash
# Motia Workbench restart (lädt neue Steps)
systemctl restart motia-workbench # oder entsprechender Befehl
```
### 3. Verify
```bash
# Check logs
tail -f /var/log/motia/workbench.log
# Test single sync
python scripts/test_beteiligte_sync.py
```
## Weitere Advoware-Syncs
Dieses System ist als **Template für alle Advoware-Syncs** designed. Wichtige Prinzipien:
1. **Redis Distributed Lock** für atomare Operations
2. **Merge Utility** für Read-Modify-Write Pattern
3. **Max Retries** mit Notification
4. **Batch Processing** in Cron
5. **Combined API Calls** wo möglich
→ Siehe [SYNC_TEMPLATE.md](SYNC_TEMPLATE.md) für Implementierungs-Template
## Siehe auch
- [Entity Mapping Details](../ENTITY_MAPPING_CBeteiligte_Advoware.md)
- [Advoware API Docs](advoware/)
- [EspoCRM API Docs](API.md)