feat(sync): Implement comprehensive sync fixes and optimizations as of February 8, 2026
- Fixed initial sync logic to respect actual timestamps, preventing unwanted overwrites. - Introduced exponential backoff for retry logic, with auto-reset for permanently failed entities. - Added validation checks to ensure data consistency during sync processes. - Corrected hash calculation to only include sync-relevant communications. - Resolved issues with empty slots ignoring user inputs and improved conflict handling. - Enhanced handling of Var4 and Var6 entries during sync conflicts. - Documented changes and added new fields required in EspoCRM for improved sync management. Also added a detailed analysis of syncStatus values in EspoCRM CBeteiligte, outlining responsibilities and ensuring robust sync mechanisms.
This commit is contained in:
522
bitbylaw/docs/archive/BETEILIGTE_SYNC.md
Normal file
522
bitbylaw/docs/archive/BETEILIGTE_SYNC.md
Normal file
@@ -0,0 +1,522 @@
|
||||
# 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 (rowId + modifiedAt vs geaendertAm):
|
||||
- no_change → Nur Kommunikation sync (direction=both)
|
||||
- espocrm_newer → Update Advoware (PUT) + Kommunikation sync (direction=both)
|
||||
- advoware_newer → Update EspoCRM (PATCH) + Kommunikation sync (direction=both)
|
||||
- conflict → EspoCRM wins (PUT) + Notification + Kommunikation sync (direction=to_advoware ONLY!)
|
||||
↓
|
||||
Kommunikation Sync (Hash-basiert, siehe unten)
|
||||
↓
|
||||
Release Lock (NACH Kommunikation-Sync!)
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
**WICHTIG**: Kommunikation-Sync läuft **IMMER** nach Stammdaten-Sync (auch bei `no_change`)!
|
||||
|
||||
### Hash-basierte Änderungserkennung ✅
|
||||
|
||||
Die Kommunikation-Synchronisation verwendet **MD5-Hash** der `kommunikation` rowIds aus Advoware:
|
||||
- **Hash-Berechnung**: MD5 von sortierten rowIds (erste 16 Zeichen)
|
||||
- **Speicherort**: `kommunikationHash` in EspoCRM CBeteiligte
|
||||
- **Vorteil**: Erkennt Kommunikations-Änderungen ohne Beteiligte-rowId-Änderung
|
||||
|
||||
**Problem gelöst**: Beteiligte-rowId ändert sich **NICHT**, wenn nur Kommunikation geändert wird!
|
||||
|
||||
### 3-Way Diffing mit Konflikt-Erkennung
|
||||
|
||||
```python
|
||||
# Timestamp-basiert für EspoCRM
|
||||
espo_changed = espo_bet.modifiedAt > espo_bet.advowareLastSync
|
||||
|
||||
# Hash-basiert für Advoware
|
||||
stored_hash = espo_bet.kommunikationHash # z.B. "a3f5d2e8b1c4f6a9"
|
||||
current_hash = MD5(sorted(komm.rowId for komm in advo_kommunikationen))[:16]
|
||||
advo_changed = stored_hash != current_hash
|
||||
|
||||
# Konflikt-Erkennung
|
||||
if espo_changed AND advo_changed:
|
||||
espo_wins = True # EspoCRM gewinnt immer!
|
||||
```
|
||||
|
||||
### Konflikt-Behandlung: EspoCRM Wins
|
||||
|
||||
**Bei Konflikt** (beide Seiten geändert):
|
||||
1. **Stammdaten**: EspoCRM → Advoware (PUT)
|
||||
2. **Kommunikation**: `direction='to_advoware'` (NUR EspoCRM→Advoware, blockiert Advoware→EspoCRM)
|
||||
3. **Notification**: In-App Benachrichtigung
|
||||
4. **Hash-Update**: Neuer Hash wird gespeichert
|
||||
|
||||
**Ohne Konflikt**:
|
||||
- **Stammdaten**: Je nach Timestamp-Vergleich
|
||||
- **Kommunikation**: `direction='both'` (bidirektional)
|
||||
|
||||
### 6 Sync-Varianten (Var1-6)
|
||||
|
||||
**Var1**: Neu in EspoCRM → CREATE in Advoware
|
||||
**Var2**: Gelöscht in EspoCRM → DELETE in Advoware (Empty Slot)
|
||||
**Var3**: Gelöscht in Advoware → DELETE in EspoCRM
|
||||
**Var4**: Neu in Advoware → CREATE in EspoCRM
|
||||
**Var5**: Geändert in EspoCRM → UPDATE in Advoware
|
||||
**Var6**: Geändert in Advoware → UPDATE in EspoCRM
|
||||
|
||||
### Base64-Marker Strategie
|
||||
```
|
||||
[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Geschäftlich
|
||||
[ESPOCRM-SLOT:4] # Leerer Slot nach Löschung
|
||||
```
|
||||
|
||||
### Base64-Marker Strategie
|
||||
|
||||
**Marker-Format** im Advoware `bemerkung` Feld:
|
||||
```
|
||||
[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
|
||||
```
|
||||
|
||||
### 4-Stufen kommKz-Erkennung (Type Detection)
|
||||
|
||||
**Problem**: Advoware `kommKz` ist via GET immer 0, via PUT read-only!
|
||||
|
||||
**Lösung - Prioritäts-Kaskade**:
|
||||
1. **Marker** (höchste Priorität) → `[ESPOCRM:...:3]` = kommKz 3 (Mobil)
|
||||
2. **EspoCRM Type** (bei EspoCRM→Advoware) → `type: 'Mobile'` = kommKz 3
|
||||
3. **Top-Level Felder** → `beteiligte.mobil` = kommKz 3
|
||||
4. **Wert-Pattern** → `@` in Wert = Email (kommKz 4)
|
||||
5. **Default** → Fallback (TelGesch=1, MailGesch=4)
|
||||
|
||||
**Mapping EspoCRM phoneNumberData.type → kommKz**:
|
||||
```python
|
||||
PHONE_TYPE_TO_KOMMKZ = {
|
||||
'Office': 1, # TelGesch
|
||||
'Fax': 2, # FaxGesch
|
||||
'Mobile': 3, # Mobil
|
||||
'Home': 6, # TelPrivat
|
||||
'Other': 10 # Sonstige
|
||||
}
|
||||
```
|
||||
|
||||
### Slot-Wiederverwendung (Empty Slots)
|
||||
|
||||
**Problem**: Advoware DELETE gibt 403 Forbidden!
|
||||
|
||||
**Lösung**: Empty Slots mit Marker
|
||||
```python
|
||||
# Gelöscht in EspoCRM → Create Empty Slot in Advoware
|
||||
{
|
||||
"tlf": "",
|
||||
"bemerkung": "[ESPOCRM-SLOT:4]", # kommKz=4 (Email)
|
||||
"kommKz": 4,
|
||||
"online": True
|
||||
}
|
||||
```
|
||||
|
||||
**Wiederverwendung**:
|
||||
- Neue Einträge prüfen zuerst Empty Slots mit passendem kommKz
|
||||
- UPDATE statt CREATE spart API-Calls und IDs
|
||||
|
||||
### Lock-Management mit Redis
|
||||
|
||||
**WICHTIG**: Lock wird erst NACH Kommunikation-Sync freigegeben!
|
||||
|
||||
```python
|
||||
# Pattern in allen 4 Szenarien:
|
||||
await sync_utils.acquire_sync_lock(entity_id)
|
||||
try:
|
||||
# 1. Stammdaten sync
|
||||
# 2. Kommunikation sync (run_kommunikation_sync helper)
|
||||
# 3. Lock release
|
||||
await sync_utils.release_sync_lock(entity_id, 'clean')
|
||||
finally:
|
||||
# Failsafe: Lock wird auch bei Exception released
|
||||
pass
|
||||
```
|
||||
|
||||
**Vorher (BUG)**: Lock wurde teilweise VOR Kommunikation-Sync released!
|
||||
**Jetzt**: Konsistentes Pattern - Lock schützt gesamte Operation
|
||||
|
||||
### Implementation Details
|
||||
|
||||
**Implementation**:
|
||||
- [kommunikation_mapper.py](../services/kommunikation_mapper.py) - Base64 encoding/decoding, kommKz detection
|
||||
- [kommunikation_sync_utils.py](../services/kommunikation_sync_utils.py) - Sync-Manager mit 3-way diffing
|
||||
- [beteiligte_sync_event_step.py](../steps/vmh/beteiligte_sync_event_step.py) - Event handler mit helper function
|
||||
- Tests: [test_kommunikation_sync_implementation.py](../scripts/test_kommunikation_sync_implementation.py)
|
||||
|
||||
**Helper Function** (DRY-Prinzip):
|
||||
```python
|
||||
async def run_kommunikation_sync(entity_id, betnr, komm_sync, context, direction='both'):
|
||||
"""Führt Kommunikation-Sync aus mit Error-Handling und Logging"""
|
||||
context.logger.info(f"📞 Starte Kommunikation-Sync (direction={direction})...")
|
||||
komm_result = await komm_sync.sync_bidirectional(entity_id, betnr, direction=direction)
|
||||
return komm_result
|
||||
```
|
||||
|
||||
**Verwendet in**:
|
||||
- no_change: `direction='both'`
|
||||
- espocrm_newer: `direction='both'`
|
||||
- advoware_newer: `direction='both'`
|
||||
- **conflict**: `direction='to_advoware'` ← NUR EspoCRM→Advoware!
|
||||
|
||||
## 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
|
||||
- `advowareRowId` (varchar, 50) - Cached Advoware rowId für Change Detection
|
||||
- **`kommunikationHash` (varchar, 16)** - MD5-Hash der Kommunikation rowIds (erste 16 Zeichen)
|
||||
- `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)
|
||||
Reference in New Issue
Block a user