# 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 ## 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)