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

12 KiB

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)

    • Subscribes: vmh.beteiligte.{create,update,delete,sync_check}
    • Verarbeitet Sync-Events
    • Verwendet Redis distributed lock
  2. Cron Job (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)

    • Lock-Management (Redis distributed lock)
    • Timestamp-Vergleich
    • Merge-Utility für Advoware PUT
    • Notifications
  4. Mapper (espocrm_mapper.py)

    • map_cbeteiligte_to_advoware() - EspoCRM → Advoware
    • map_advoware_to_cbeteiligte() - Advoware → EspoCRM
    • Nur Stammdaten, keine Kontaktdaten
  5. APIs

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

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)

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)

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)

merged = sync_utils.merge_for_advoware_put(advo_entity, espo_entity, mapper)
  • Keine Code-Duplikation
  • Konsistentes Logging
  • Wiederverwendbar

4. Max Retry Limit (Robustheit)

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)

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:
    # 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 Felderbeteiligte.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:

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

-- In EspoCRM
SELECT syncStatus, COUNT(*) 
FROM c_beteiligte 
GROUP BY syncStatus;

Failed Syncs

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

cd /opt/motia-app/bitbylaw
source python_modules/bin/activate
python scripts/test_beteiligte_sync.py

Manual Test

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

# 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

# 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

cd /opt/motia-app/bitbylaw
git pull
source python_modules/bin/activate
pip install -r requirements.txt

2. Restart Motia

# Motia Workbench restart (lädt neue Steps)
systemctl restart motia-workbench  # oder entsprechender Befehl

3. Verify

# 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 für Implementierungs-Template

Siehe auch