# Sync-Strategie: EspoCRM ↔ Advoware Beteiligte **Analysiert am**: 2026-02-07 **Anforderungen**: - a) EspoCRM Update β†’ Advoware Update - b) Bi-direktionaler Sync mit KonfliktauflΓΆsung - c) Neue Beteiligte in EspoCRM β†’ Neue in Advoware (Status-Feld) - d) Cron-basierter Sync **Problem**: EspoCRM hat Webhooks, Advoware nicht. --- ## πŸ“Š Bestehende Architektur (aus Calendar Sync) Das bestehende **Calendar Sync System** bietet ein exzellentes Template: ### Aktuelle Komponenten: 1. **Webhook-Receiver**: `beteiligte_create/update/delete_api_step.py` βœ“ Bereits vorhanden 2. **Event-Handler**: `beteiligte_sync_event_step.py` ⚠️ Placeholder 3. **Cron-Job**: Analog zu `calendar_sync_cron_step.py` 4. **PostgreSQL State DB**: FΓΌr Sync-State und KonfliktauflΓΆsung 5. **Redis**: Deduplication + Rate Limiting ### BewΓ€hrte Patterns aus Calendar Sync: **1. Datenbank-Schema** (PostgreSQL): ```sql CREATE TABLE beteiligte_sync ( sync_id SERIAL PRIMARY KEY, employee_kuerzel VARCHAR(10), -- IDs beider Systeme espocrm_id VARCHAR(255) UNIQUE, advoware_betnr INTEGER UNIQUE, -- Metadaten source_system VARCHAR(20), -- 'espocrm' oder 'advoware' sync_strategy VARCHAR(50) DEFAULT 'source_system_wins', sync_status VARCHAR(20) DEFAULT 'synced', -- 'synced', 'pending', 'failed', 'conflict' -- Timestamps espocrm_modified_at TIMESTAMP WITH TIME ZONE, advoware_modified_at TIMESTAMP WITH TIME ZONE, last_sync TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Flags deleted BOOLEAN DEFAULT FALSE, advoware_write_allowed BOOLEAN DEFAULT TRUE, -- Audit created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE INDEX idx_espocrm_id ON beteiligte_sync(espocrm_id); CREATE INDEX idx_advoware_betnr ON beteiligte_sync(advoware_betnr); CREATE INDEX idx_sync_status ON beteiligte_sync(sync_status); CREATE INDEX idx_deleted ON beteiligte_sync(deleted) WHERE NOT deleted; ``` **2. Sync-Phasen** (3-Phasen-Modell): - **Phase 1**: Neue aus EspoCRM β†’ Advoware - **Phase 2**: Neue aus Advoware β†’ EspoCRM - **Phase 3**: Updates + KonfliktauflΓΆsung - **Phase 4**: Deletes **3. KonfliktauflΓΆsung via Timestamps**: ```python # Aus calendar_sync_event_step.py, Zeile 870+ if espo_ts and advo_ts: if espo_ts > advo_ts: # EspoCRM ist neuer β†’ Update Advoware await update_advoware(...) elif advo_ts > espo_ts: # Advoware ist neuer β†’ Update EspoCRM await update_espocrm(...) else: # Gleich alt β†’ Skip pass ``` --- ## 🎯 Empfohlene Architektur fΓΌr Beteiligte-Sync ### Komponenten-Übersicht ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EspoCRM β”‚ β”‚ β”‚ β”‚ CBeteiligte Entity β”‚ β”‚ - Webhooks: create/update/delete β”‚ β”‚ - Felder: betnr, syncStatus, advowareLastSync β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Webhook (HTTP POST) β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ KONG API Gateway β†’ Motia Framework β”‚ β”‚ β”‚ β”‚ 1. beteiligte_create/update/delete_api_step.py β”‚ β”‚ - EmpfΓ€ngt Webhooks β”‚ β”‚ - Dedupliziert via Redis β”‚ β”‚ - Emittiert Events β”‚ β”‚ β”‚ β”‚ 2. beteiligte_sync_event_step.py β”‚ β”‚ - Subscribed zu Events β”‚ β”‚ - Holt vollstΓ€ndige Entity aus EspoCRM β”‚ β”‚ - Transformiert via Mapper β”‚ β”‚ - Schreibt nach Advoware β”‚ β”‚ - Updated PostgreSQL Sync-State β”‚ β”‚ β”‚ β”‚ 3. beteiligte_sync_cron_step.py (NEU) β”‚ β”‚ - LΓ€uft alle 15 Minuten β”‚ β”‚ - Emittiert "beteiligte.sync_all" Event β”‚ β”‚ β”‚ β”‚ 4. beteiligte_sync_all_event_step.py (NEU) β”‚ β”‚ - Fetcht alle Beteiligte aus Advoware β”‚ β”‚ - Vergleicht mit PostgreSQL State β”‚ β”‚ - Identifiziert Neue/GeΓ€nderte/GelΓΆschte in Advoware β”‚ β”‚ - Sync nach EspoCRM β”‚ β”‚ - 3-Phasen-Modell wie Calendar Sync β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ PostgreSQL Sync-State DB β”‚ β”‚ β”‚ β”‚ Tabelle: beteiligte_sync β”‚ β”‚ - Mapping: espocrm_id ↔ advoware_betnr β”‚ β”‚ - Timestamps beider Systeme β”‚ β”‚ - Sync-Status & Konfliktflags β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Redis (Caching & Deduplication) β”‚ β”‚ β”‚ β”‚ - vmh:beteiligte:create_pending (SET) β”‚ β”‚ - vmh:beteiligte:update_pending (SET) β”‚ β”‚ - vmh:beteiligte:delete_pending (SET) β”‚ β”‚ - vmh:beteiligte:sync_lock:{espocrm_id} (Key mit TTL) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Advoware API β”‚ β”‚ β”‚ β”‚ /api/v1/advonet/Beteiligte β”‚ β”‚ - GET: Liste + Einzelabfrage β”‚ β”‚ - POST: Create β”‚ β”‚ - PUT: Update β”‚ β”‚ - DELETE: Delete β”‚ β”‚ β”‚ β”‚ ⚠️ KEIN Webhook-Support β”‚ β”‚ β†’ Polling via Cron erforderlich β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## πŸ”„ Detaillierte Sync-Flows ### Flow A: EspoCRM Update β†’ Advoware (Webhook-getrieben) **Trigger**: EspoCRM sendet Webhook bei Create/Update/Delete ``` 1. EspoCRM: User Γ€ndert CBeteiligte Entity └─> Webhook: POST /vmh/webhook/beteiligte/update Body: [{"id": "68e4af00172be7924", ...}] 2. beteiligte_update_api_step.py: β”œβ”€> Deduplizierung via Redis SET β”œβ”€> Neue IDs β†’ Redis: vmh:beteiligte:update_pending └─> Emit Event: "vmh.beteiligte.update" 3. beteiligte_sync_event_step.py: β”œβ”€> Receive Event mit entity_id β”œβ”€> Fetch full entity von EspoCRM API β”‚ GET /api/v1/CBeteiligte/{entity_id} β”‚ β”œβ”€> Check PostgreSQL Sync-State: β”‚ SELECT * FROM beteiligte_sync WHERE espocrm_id = ? β”‚ β”œβ”€> Falls NEU (nicht in DB): β”‚ β”œβ”€> Check syncStatus in EspoCRM: β”‚ β”‚ - "pending_sync" β†’ Create in Advoware β”‚ β”‚ - "clean" β†’ Skip (bereits gesynct von anderem Flow) β”‚ β”‚ β”‚ β”œβ”€> Transform via Mapper: β”‚ β”‚ espocrm_mapper.map_cbeteiligte_to_advoware(entity) β”‚ β”‚ β”‚ β”œβ”€> POST /api/v1/advonet/Beteiligte β”‚ β”‚ β†’ Response: {betNr: 123456, ...} β”‚ β”‚ β”‚ β”œβ”€> Insert in PostgreSQL: β”‚ β”‚ INSERT INTO beteiligte_sync ( β”‚ β”‚ espocrm_id, advoware_betnr, β”‚ β”‚ source_system = 'espocrm', β”‚ β”‚ espocrm_modified_at = entity.modifiedAt β”‚ β”‚ ) β”‚ β”‚ β”‚ └─> Update EspoCRM: β”‚ PUT /api/v1/CBeteiligte/{entity_id} β”‚ { β”‚ betnr: 123456, β”‚ syncStatus: "clean", β”‚ advowareLastSync: NOW() β”‚ } β”‚ └─> Falls EXISTIERT (in DB): β”œβ”€> Get Advoware timestamp: β”‚ Fetch /api/v1/advonet/Beteiligte/{betnr} β”‚ β†’ advoware.geaendertAm β”‚ β”œβ”€> KonfliktauflΓΆsung: β”‚ IF espocrm.modifiedAt > advoware.geaendertAm: β”‚ β†’ Update Advoware (EspoCRM gewinnt) β”‚ PUT /api/v1/advonet/Beteiligte/{betnr} β”‚ ELSE IF advoware.geaendertAm > espocrm.modifiedAt: β”‚ β†’ Update EspoCRM (Advoware gewinnt) β”‚ PUT /api/v1/CBeteiligte/{entity_id} β”‚ ELSE: β”‚ β†’ Skip (gleich alt) β”‚ └─> Update PostgreSQL: UPDATE beteiligte_sync SET espocrm_modified_at = ..., advoware_modified_at = ..., last_sync = NOW(), sync_status = 'synced' 4. Cleanup: └─> Redis: SREM vmh:beteiligte:update_pending {entity_id} ``` **Timing**: ~2-5 Sekunden nach Γ„nderung in EspoCRM --- ### Flow B: Advoware Update β†’ EspoCRM (Polling via Cron) **Trigger**: Cron-Job alle 15 Minuten ``` 1. beteiligte_sync_cron_step.py (Cron: */15 * * * *): └─> Emit Event: "beteiligte.sync_all" 2. beteiligte_sync_all_event_step.py: β”œβ”€> Fetch alle Beteiligte aus PostgreSQL Sync-DB: β”‚ SELECT * FROM beteiligte_sync WHERE NOT deleted β”‚ β”œβ”€> Fetch alle Beteiligte aus Advoware: β”‚ GET /api/v1/advonet/Beteiligte β”‚ (Optional mit Filter: nur geΓ€ndert seit last_sync - 1 Tag) β”‚ β”œβ”€> Build Maps: β”‚ db_map = {betnr: row} β”‚ advo_map = {betNr: entity} β”‚ β”œβ”€> PHASE 1: Neue in Advoware (nicht in DB): β”‚ FOR betnr IN advo_map: β”‚ IF betnr NOT IN db_map: β”‚ β”œβ”€> Transform: map_advoware_to_cbeteiligte(advo_entity) β”‚ β”‚ β”‚ β”œβ”€> Check if exists in EspoCRM: β”‚ β”‚ Search by name/email (Fuzzy Match) β”‚ β”‚ β”‚ β”œβ”€> IF NOT EXISTS: β”‚ β”‚ β”œβ”€> POST /api/v1/CBeteiligte β”‚ β”‚ β”‚ { β”‚ β”‚ β”‚ ...fields..., β”‚ β”‚ β”‚ betnr: {betnr}, β”‚ β”‚ β”‚ syncStatus: "clean", β”‚ β”‚ β”‚ advowareLastSync: NOW() β”‚ β”‚ β”‚ } β”‚ β”‚ β”‚ β”‚ β”‚ └─> INSERT INTO beteiligte_sync ( β”‚ β”‚ espocrm_id = new_id, β”‚ β”‚ advoware_betnr = betnr, β”‚ β”‚ source_system = 'advoware' β”‚ β”‚ ) β”‚ β”‚ β”‚ └─> ELSE (Match gefunden): β”‚ └─> UPDATE beteiligte_sync SET β”‚ advoware_betnr = betnr, β”‚ source_system = 'merged' β”‚ β”œβ”€> PHASE 2: Updates (beide vorhanden): β”‚ FOR row IN db_map: β”‚ IF row.advoware_betnr IN advo_map: β”‚ advo_entity = advo_map[row.advoware_betnr] β”‚ espo_entity = fetch_from_espocrm(row.espocrm_id) β”‚ β”‚ β”œβ”€> Get Timestamps: β”‚ β”‚ advo_ts = advo_entity.geaendertAm β”‚ β”‚ espo_ts = espo_entity.modifiedAt β”‚ β”‚ last_sync_ts = row.last_sync β”‚ β”‚ β”‚ β”œβ”€> KonfliktauflΓΆsung: β”‚ β”‚ IF advo_ts > espo_ts AND advo_ts > last_sync_ts: β”‚ β”‚ β†’ Advoware ist neuer β”‚ β”‚ PUT /api/v1/CBeteiligte/{espocrm_id} β”‚ β”‚ (Update EspoCRM mit Advoware-Daten) β”‚ β”‚ β”‚ β”‚ ELSE IF espo_ts > advo_ts AND espo_ts > last_sync_ts: β”‚ β”‚ β†’ EspoCRM ist neuer β”‚ β”‚ (Wurde bereits in Flow A behandelt, skip) β”‚ β”‚ β”‚ β”‚ ELSE IF advo_ts == espo_ts: β”‚ β”‚ β†’ Keine Γ„nderung, skip β”‚ β”‚ β”‚ β”‚ ELSE IF advo_ts > last_sync_ts AND espo_ts > last_sync_ts: β”‚ β”‚ β†’ KONFLIKT: Beide seit last_sync geΓ€ndert β”‚ β”‚ β”œβ”€> Strategy: "advoware_wins" (konfigurierbar) β”‚ β”‚ β”œβ”€> UPDATE mit Winner-Daten β”‚ β”‚ β”œβ”€> Log Conflict β”‚ β”‚ └─> SET sync_status = 'conflict_resolved' β”‚ β”‚ β”‚ └─> UPDATE beteiligte_sync SET β”‚ espocrm_modified_at = espo_ts, β”‚ advoware_modified_at = advo_ts, β”‚ last_sync = NOW(), β”‚ sync_status = 'synced' β”‚ └─> PHASE 3: Deletes (in DB, nicht in Advoware): FOR row IN db_map: IF row.advoware_betnr NOT IN advo_map: β”œβ”€> Check if exists in EspoCRM: β”‚ GET /api/v1/CBeteiligte/{espocrm_id} β”‚ β”œβ”€> IF EXISTS: β”‚ β”œβ”€> Soft-Delete in EspoCRM: β”‚ β”‚ PUT /api/v1/CBeteiligte/{espocrm_id} β”‚ β”‚ {deleted: true, syncStatus: "deleted"} β”‚ β”‚ β”‚ └─> UPDATE beteiligte_sync SET β”‚ deleted = TRUE, β”‚ sync_status = 'synced' β”‚ └─> ELSE (auch in EspoCRM nicht da): └─> UPDATE beteiligte_sync SET deleted = TRUE ``` **Timing**: Alle 15 Minuten (konfigurierbar) --- ## πŸŽ›οΈ Status-Feld in EspoCRM ### Feld: `syncStatus` (String, Custom Field) **Werte**: - `"pending_sync"` β†’ Neu erstellt, wartet auf Sync nach Advoware - `"clean"` β†’ Synchronisiert, keine Γ„nderungen - `"dirty"` β†’ GeΓ€ndert seit letztem Sync, wartet auf Sync - `"syncing"` β†’ Sync lΓ€uft gerade - `"failed"` β†’ Sync fehlgeschlagen (+ Fehlerlog) - `"conflict"` β†’ Konflikt detektiert - `"deleted"` β†’ In Advoware gelΓΆscht **Zusatzfeld**: `advowareLastSync` (DateTime) ### Alternative: PostgreSQL als Single Source of Truth Statt `syncStatus` in EspoCRM: - Alle Status in PostgreSQL `beteiligte_sync.sync_status` - EspoCRM hat nur `betnr` und `advowareLastSync` - **Vorteil**: Keine Schema-Γ„nderung in EspoCRM nΓΆtig - **Nachteil**: Status nicht direkt in EspoCRM UI sichtbar **Empfehlung**: Beides nutzen - PostgreSQL: Master-Status fΓΌr Sync-Logic - EspoCRM: Read-only Display fΓΌr User --- ## βš™οΈ Cron-Job Konfiguration ### Option 1: Separate Cron-Steps (empfohlen) ```python # beteiligte_sync_cron_step.py config = { 'type': 'cron', 'name': 'Beteiligte Sync Cron', 'cron': '*/15 * * * *', # Alle 15 Minuten 'emits': ['beteiligte.sync_all'], 'flows': ['vmh'] } ``` ### Option 2: Integriert in bestehenden Cron ```python # Kombiniert mit anderen Sync-Tasks config = { 'type': 'cron', 'name': 'All Syncs Cron', 'cron': '*/5 * * * *', # Alle 5 Minuten 'emits': ['calendar_sync_all', 'beteiligte.sync_all'], 'flows': ['advoware', 'vmh'] } ``` ### Timing-Überlegungen: **Frequenz**: - **15 Minuten**: Guter Kompromiss (wie bestehende Calendar Sync) - **5 Minuten**: Wenn schnellere Reaktion auf Advoware-Γ„nderungen nΓΆtig - **1 Stunde**: Wenn Last auf APIs minimiert werden soll **Offset**: - Calendar Sync: 0 Minuten - Beteiligte Sync: +5 Minuten - β†’ Verhindert API-Überlast durch gleichzeitige Requests --- ## πŸ” Sicherheit & Fehlerbehandlung ### 1. Rate Limiting **Advoware API** (aus Calendar Sync): ```python # Bereits implementiert in AdvowareAPI Service # - Token-basiertes Rate Limiting via Redis # - Backoff-Strategie bei 429 Errors ``` **EspoCRM API**: ```python # Zu implementieren in EspoCRMAPI Service ESPOCRM_RATE_LIMIT_KEY = 'espocrm_api_tokens' MAX_TOKENS = 100 # Basierend auf API-Limits REFILL_RATE = 100 / 60 # Tokens pro Sekunde ``` ### 2. Lock-Mechanismus (Verhindert Race Conditions) ```python # Redis Lock fΓΌr Entity wΓ€hrend Sync lock_key = f'vmh:beteiligte:sync_lock:{entity_id}' if redis.set(lock_key, '1', nx=True, ex=300): # 5 Min TTL try: await perform_sync(entity_id) finally: redis.delete(lock_key) else: logger.warning(f"Entity {entity_id} ist bereits im Sync-Prozess") ``` ### 3. Retry-Logic mit Exponential Backoff ```python import backoff @backoff.on_exception( backoff.expo, (AdvowareAPIError, EspoCRMAPIError), max_tries=3, max_time=60 ) async def sync_entity_with_retry(entity_id): # ... Sync-Logic pass ``` ### 4. Fehler-Logging & Monitoring ```python # PostgreSQL Error-Log Tabelle CREATE TABLE beteiligte_sync_errors ( error_id SERIAL PRIMARY KEY, sync_id INTEGER REFERENCES beteiligte_sync(sync_id), error_type VARCHAR(50), error_message TEXT, error_stack TEXT, retry_count INTEGER DEFAULT 0, resolved BOOLEAN DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); ``` ### 5. Write-Protection Flag **Global** (Config): ```python # config.py ADVOWARE_WRITE_PROTECTION = os.getenv('ADVOWARE_WRITE_PROTECTION', 'false').lower() == 'true' ``` **Per-Entity** (DB): ```sql ALTER TABLE beteiligte_sync ADD COLUMN advoware_write_allowed BOOLEAN DEFAULT TRUE; ``` --- ## πŸ“¦ Zu implementierende Module ### PrioritΓ€t 1 (Core Sync): 1. **services/espocrm_mapper.py** ⭐⭐⭐ - `map_cbeteiligte_to_advoware(espo_entity) -> advo_data` - `map_advoware_to_cbeteiligte(advo_entity) -> espo_data` - Feld-Mapping gemÀß `ENTITY_MAPPING_CBeteiligte_Advoware.md` 2. **steps/vmh/beteiligte_sync_event_step.py** ⭐⭐⭐ - Implementiert Flow A (Webhook β†’ Advoware) - Subscribe to create/update/delete Events - PostgreSQL Integration - KonfliktauflΓΆsung 3. **PostgreSQL Migration** ⭐⭐⭐ - `migrations/001_create_beteiligte_sync_table.sql` - Connection in Config 4. **steps/vmh/beteiligte_sync_cron_step.py** ⭐⭐ - Emittiert Sync-All Event alle 15 Min 5. **steps/vmh/beteiligte_sync_all_event_step.py** ⭐⭐ - Implementiert Flow B (Advoware Polling) - 3-Phasen-Sync-Modell ### PrioritΓ€t 2 (Optimierungen): 6. **services/beteiligte_sync_utils.py** ⭐ - Shared Utilities - Lock-Management - Timestamp-Handling - Conflict-Resolution Logic 7. **Testing** ⭐ - Unit Tests fΓΌr Mapper - Integration Tests fΓΌr Sync-Flows - Konflikt-Szenarien ### PrioritΓ€t 3 (Monitoring): 8. **steps/vmh/audit_beteiligte_sync.py** - Analog zu `audit_calendar_sync.py` - CLI-Tool fΓΌr Sync-Status 9. **Dashboard/Metrics** - Prometheus Metrics - Grafana Dashboard --- ## πŸš€ Rollout-Plan ### Phase 1: Setup (Tag 1-2) - [ ] PostgreSQL Datenbank-Schema erstellen - [ ] Config erweitern (DB-Connection) - [ ] Mapper-Modul erstellen - [ ] Unit Tests fΓΌr Mapper ### Phase 2: Webhook-Flow (Tag 3-4) - [ ] `beteiligte_sync_event_step.py` implementieren - [ ] Integration mit bestehenden Webhook-Steps - [ ] Testing mit EspoCRM Sandbox ### Phase 3: Polling-Flow (Tag 5-6) - [ ] Cron-Step erstellen - [ ] `beteiligte_sync_all_event_step.py` implementieren - [ ] 3-Phasen-Modell - [ ] Integration Tests ### Phase 4: KonfliktauflΓΆsung (Tag 7) - [ ] Timestamp-Vergleich - [ ] Konflikt-Strategies - [ ] Error-Handling - [ ] Retry-Logic ### Phase 5: Monitoring & Docs (Tag 8) - [ ] Audit-Tool - [ ] Logging - [ ] Dokumentation - [ ] Runbook ### Phase 6: Production (Tag 9-10) - [ ] Staging-Tests - [ ] Performance-Tests - [ ] Production-Rollout - [ ] Monitoring --- ## ⚠️ Wichtige Entscheidungen ### 1. Conflict Resolution Strategy **Option A: "Source System Wins"** (empfohlen fΓΌr Start): ```python sync_strategy = 'source_system_wins' # EspoCRM-created Entities β†’ EspoCRM gewinnt bei Konflikt # Advoware-created Entities β†’ Advoware gewinnt bei Konflikt ``` **Option B: "Advoware Always Wins"**: ```python sync_strategy = 'advoware_master' # Advoware ist Master, EspoCRM ist read-only View ``` **Option C: "Last Modified Wins"**: ```python sync_strategy = 'last_modified_wins' # Timestamp-Vergleich, neuester gewinnt ``` **Empfehlung**: Start mit "Source System Wins", spΓ€ter auf "Last Modified Wins" mit Manual Conflict Review. ### 2. Advoware Polling-Frequenz **Überlegungen**: - API-Last auf Advoware - AktualitΓ€ts-Anforderungen - Anzahl Beteiligte (~1000? ~10.000?) **Optimierung**: - Incremental Fetch: Nur geΓ€ndert seit `last_sync - 1 Tag` - Delta-Detection via `geaendertAm` Timestamp - Pagination bei großen Datenmengen ### 3. EspoCRM Field: `syncStatus` **Zu klΓ€ren**: - Bestehendes Custom Field oder neu anlegen? - Dropdown-Werte konfigurieren - Permissions (nur Sync-System kann schreiben?) --- ## πŸ“ Zusammenfassung ### Was funktioniert: βœ… EspoCRM Webhooks β†’ Motia Events βœ… Webhook-Deduplication via Redis βœ… EspoCRM API Client βœ… Advoware API Client βœ… Entity-Mapping definiert ### Was zu implementieren ist: πŸ”¨ Mapper-Modul πŸ”¨ PostgreSQL Sync-State DB πŸ”¨ Event-Handler fΓΌr Webhooks πŸ”¨ Cron-Job fΓΌr Polling πŸ”¨ 3-Phasen-Sync fΓΌr Advoware β†’ EspoCRM πŸ”¨ KonfliktauflΓΆsung πŸ”¨ Error-Handling & Monitoring ### GeschΓ€tzter Aufwand: - **Setup & Core**: 3-4 Tage - **Flows**: 2-3 Tage - **KonfliktauflΓΆsung**: 1-2 Tage - **Testing & Docs**: 1-2 Tage - **Rollout**: 1-2 Tage - **Total**: ~8-13 Tage ### NΓ€chster Schritt: 1. Entscheidung zu Status-Feld in EspoCRM 2. PostgreSQL DB-Schema aufsetzen 3. Mapper-Modul implementieren 4. Webhook-Flow komplettieren --- **Fragen zur KlΓ€rung**: 1. Existiert `syncStatus` Field bereits in EspoCRM CBeteiligte? 2. Wie viele Beteiligte gibt es ca. in Advoware? 3. Gibt es Performance-Anforderungen? (z.B. Sync innerhalb X Sekunden) 4. Soll es manuelle Conflict-Resolution geben oder automatisch? 5. PostgreSQL Server bereits vorhanden? (Wie Calendar Sync)