- Introduced SYNC_STRATEGY_ARCHIVE.md detailing the sync process, status values, and flow for updating entities from EspoCRM to Advoware and vice versa. - Created SYNC_TEMPLATE.md as a guide for implementing new syncs, including field definitions, mapper examples, sync utilities, event handlers, and cron jobs. - Added README_SYNC.md for the Beteiligte sync event handler, outlining its functionality, event subscriptions, optimizations, error handling, and performance metrics.
360 lines
9.7 KiB
Markdown
360 lines
9.7 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
|
|
|
|
## 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)
|