# Advoware Sync - System-Übersicht

**Letzte Aktualisierung**: 8. Februar 2026  
**Status**: ✅ Production Ready

---

## Inhaltsverzeichnis

1. [System-Architektur](#system-architektur)
2. [Beteiligte Sync (Stammdaten)](#beteiligte-sync-stammdaten)
3. [Kommunikation Sync](#kommunikation-sync)
4. [Sync Status Management](#sync-status-management)
5. [Bekannte Einschränkungen](#bekannte-einschränkungen)
6. [Troubleshooting](#troubleshooting)

---

## System-Architektur

### Defense in Depth: Webhook + Cron

Das Sync-System verwendet **zwei parallele Mechanismen** für maximale Zuverlässigkeit:

```
┌──────────────────────────────────────────────────────────┐
│                     EspoCRM                              │
│  CBeteiligte Entity (Stammdaten + Kommunikation)        │
└────────────┬────────────────────────────────────┬────────┘
             │                                    │
    Webhook (Echtzeit)                   Cron (Fallback)
             │                                    │
             ▼                                    ▼
    ┌────────────────┐                  ┌────────────────┐
    │ Event Handler  │                  │  Cron Job      │
    │ (Event-driven) │◄─────────────────│  (alle 15min)  │
    └────────┬───────┘                  └────────────────┘
             │                                   
             │ vmh.beteiligte.{create,update,delete,sync_check}
             ▼
    ┌─────────────────────────────────────────────────────┐
    │           Sync Event Handler                        │
    │  1. Redis Lock (verhindert Race Conditions)         │
    │  2. Beteiligte Sync (Stammdaten)                    │
    │     - rowId-basierte Change Detection               │
    │     - Timestamp-Vergleich für Konflikte             │
    │  3. Kommunikation Sync (Phone/Email/Fax)            │
    │     - Hash-basierte Change Detection                │
    │     - Marker-Strategy für Bidirectional Matching    │
    │  4. Lock Release                                    │
    └─────────────────────────────────────────────────────┘
             │
             ▼
    ┌─────────────────────────────────────────────────────┐
    │                 Advoware REST API                   │
    │  - POST/PUT/GET /Beteiligte (Stammdaten)           │
    │  - POST/PUT /Kommunikationen (Phone/Email/Fax)     │
    └─────────────────────────────────────────────────────┘
```

### Warum beide Mechanismen?

**Webhook (Primary)**:
- ✅ Echtzeit-Sync bei Änderungen
- ✅ Schnelles User-Feedback
- ❌ Kann fehlschlagen (Netzwerk, Server down, Race Conditions)

**Cron (Fallback)**:
- ✅ Garantierte Synchronisation alle 15 Minuten
- ✅ Findet "verlorene" Entities (pending_sync, dirty, failed)
- ✅ Auto-Retry für fehlgeschlagene Syncs
- ❌ Bis zu 15 Minuten Verzögerung

### Komponenten

| Komponente | Datei | Beschreibung |
|------------|-------|--------------|
| **Event Handler** | [beteiligte_sync_event_step.py](../steps/vmh/beteiligte_sync_event_step.py) | Central Sync Orchestrator |
| **Cron Job** | [beteiligte_sync_cron_step.py](../steps/vmh/beteiligte_sync_cron_step.py) | 15-Minuten Fallback |
| **Beteiligte Sync** | [beteiligte_sync_utils.py](../services/beteiligte_sync_utils.py) | Stammdaten-Sync Logik |
| **Kommunikation Sync** | [kommunikation_sync_utils.py](../services/kommunikation_sync_utils.py) | Phone/Email/Fax Sync |
| **Mapper** | [espocrm_mapper.py](../services/espocrm_mapper.py) | Entity Transformations |
| **Advoware API** | [advoware.py](../services/advoware.py) | HMAC-512 Auth REST Client |
| **EspoCRM API** | [espocrm.py](../services/espocrm.py) | X-Api-Key REST Client |

---

## Beteiligte Sync (Stammdaten)

### Scope

**Synchronisierte Felder** (8 Stammdatenfelder):
- `name` - Nachname / Firmenname (max 140 chars)
- `vorname` - Vorname bei natürlichen Personen (max 30 chars)
- `rechtsform` - Rechtsform (max 50 chars, muss in Advoware `/Rechtsformen` existieren)
- `titel` - Akademischer Titel (max 50 chars)
- `anrede` - Anrede (max 35 chars)
- `bAnrede` - Briefanrede (max 150 chars)
- `zusatz` - Zusatzinformation (max 100 chars)
- `geburtsdatum` - Geburtsdatum (datetime)

**NICHT synchronisiert**:
- ❌ Kontaktdaten (Telefon, Email, Fax) → separate Kommunikation Sync
- ❌ Adressen → separate Adressen Sync (geplant)
- ❌ Bankverbindungen → separate Endpoints (TBD)

### Change Detection: rowId

**Advoware rowId**: Base64-String, ändert sich bei **jedem** Advoware PUT:

```python
# Beispiel
GET /api/v1/advonet/Beteiligte/104860
{
  "betNr": 104860,
  "rowId": "FBABAAAANJFGABAAGJDOAEAPAAAAAPGFPDAFAAAA",  # Base64
  "name": "Mustermann",
  "geaendertAm": "2026-02-08T10:30:00"
}

# Nach PUT → rowId ÄNDERT sich
PUT /api/v1/advonet/Beteiligte/104860
Response:
{
  "rowId": "GBABAAAANJFGABAAGJDOAEAPAAAAAPGFPDAFAAAA",  # NEU!
  ...
}
```

**Vergleich-Logik**:
```python
espo_rowid = espo_bet.get('advowareRowId')  # Gespeichert in EspoCRM
advo_rowid = advo_bet.get('rowId')          # Aktuell aus Advoware

if espo_rowid != advo_rowid:
    # Advoware wurde geändert!
    advo_changed = True
```

### Konflikt-Behandlung

**Erkennung**: Beide Seiten haben sich seit letztem Sync geändert

```python
# EspoCRM: Timestamp-Vergleich
espo_modified = espo_bet.get('modifiedAt')
last_sync = espo_bet.get('advowareLastSync')
espo_changed = espo_modified > last_sync

# Advoware: rowId-Vergleich  
advo_changed = espo_rowid != advo_rowid

# Konflikt?
if espo_changed AND advo_changed:
    conflict = True
```

**Auflösung**: EspoCRM wins IMMER!

```python
if conflict:
    # 1. Stammdaten: EspoCRM → Advoware
    await advoware.put_beteiligte(betnr, espo_data)
    
    # 2. Kommunikation: NUR EspoCRM → Advoware (direction='to_advoware')
    await komm_sync.sync_bidirectional(entity_id, betnr, direction='to_advoware')
    
    # 3. Notification an User
    await espocrm.create_notification(
        user_id, 
        "Konflikt gelöst: EspoCRM-Daten wurden übernommen"
    )
```

**Warum Kommunikation direction='to_advoware'?**  
Bei Konflikten sollen Advoware-Änderungen NICHT zurück zu EspoCRM übertragen werden, da sonst der Konflikt wieder auftritt (Ping-Pong).

### Sync-Ablauf

#### 1. CREATE (Neu in EspoCRM)

```
User creates CBeteiligte in EspoCRM
  ↓
EspoCRM Webhook → vmh.beteiligte.create
  ↓
Event Handler:
  1. Acquire Redis Lock (5min TTL)
  2. Set syncStatus='syncing'
  3. Map CBeteiligte → BeteiligterParameter
  4. POST /api/v1/advonet/Beteiligte
     Response: {betNr: 104860, rowId: "FBAB..."}
  5. Update EspoCRM:
     - betnr = 104860
     - advowareRowId = "FBAB..."
     - advowareLastSync = NOW
     - syncStatus = 'clean'
  6. Kommunikation Sync (meist leer bei CREATE)
  7. Release Redis Lock

Result: Entity existiert in beiden Systemen mit betNr-Link
```

#### 2. UPDATE (Änderung in EspoCRM)

```
User updates CBeteiligte in EspoCRM
  ↓
EspoCRM Webhook → vmh.beteiligte.update
  ↓
Event Handler:
  1. Acquire Redis Lock
  2. Set syncStatus='syncing'
  3. GET /api/v1/advonet/Beteiligte/{betnr}
  4. Timestamp-Vergleich:
  
     Case A: no_change (beide unverändert)
       → Skip Stammdaten, nur Kommunikation Sync
     
     Case B: espocrm_newer (nur EspoCRM geändert)
       → PUT Advoware, Kommunikation Sync (both)
     
     Case C: advoware_newer (nur Advoware geändert)
       → PATCH EspoCRM, Kommunikation Sync (both)
     
     Case D: conflict (beide geändert)
       → PUT Advoware (EspoCRM wins!)
       → Kommunikation Sync (to_advoware ONLY!)
       → Notification
  
  5. Update EspoCRM (rowId, lastSync, syncStatus)
  6. Release Lock

Result: Beide Systeme synchronisiert
```

#### 3. DELETE (Gelöscht in EspoCRM)

**Problem**: Advoware DELETE ist NICHT möglich (403 Forbidden)

**Strategie**: Soft-Delete mit Notification

```
User deletes CBeteiligte in EspoCRM
  ↓
EspoCRM Webhook → vmh.beteiligte.delete
  ↓
Event Handler:
  1. NO API Call zu Advoware (würde 403 geben)
  2. Create Notification:
     "Beteiligter wurde in EspoCRM gelöscht.
      Bitte manuell in Advoware löschen: betNr 104860"
  3. Set syncStatus='deleted_in_espocrm' (optional)

Result: Entity nur in EspoCRM gelöscht, User muss manuell in Advoware löschen
```

### Bekannte Einschränkungen

#### 1. Nur 8 von 14 Advoware-Feldern funktionieren

**Funktionierende Felder**:
✅ name, vorname, rechtsform, titel, anrede, bAnrede, zusatz, geburtsdatum

**Ignorierte Felder** (im Swagger definiert, aber PUT ignoriert sie):
❌ art, kurzname, geburtsname, familienstand, handelsRegisterNummer, registergericht

**Workaround**: Mapper verwendet nur die 8 funktionierenden Felder. Handelsregister-Daten müssen manuell in Advoware gepflegt werden.

#### 2. Advoware DELETE gibt 403

**Problem**: `/api/v1/advonet/Beteiligte/{betNr}` DELETE → 403 Forbidden

**Workaround**: Soft-Delete mit Notification, User löscht manuell in Advoware.

#### 3. rowId ändert sich bei jedem PUT

**Problem**: Auch wenn gleiche Werte geschrieben werden, ändert sich rowId

**Auswirkung**: 
- Jeder Sync in Advoware invalididiert EspoCRM rowId
- Timestamp ist wichtig für echte Änderungserkennung

**Workaround**: Timestamp-Vergleich zusätzlich zu rowId-Vergleich.

#### 4. Keine Batch-Updates

**Problem**: Advoware API hat keinen Batch-Endpoint

**Auswirkung**: Bei vielen Entities viele API-Calls (N+1 Problem)

**Workaround**: 
- Cron lädt max 100 Entities pro Run
- Priorisierung: pending_sync > dirty > failed > clean (>24h)

---

## Kommunikation Sync

### Scope

**Synchronisierte Kommunikationstypen** (kommKz-Enum):

| kommKz | Name | EspoCRM Type |
|--------|------|--------------|
| 1 | TelGesch | Office (Phone) |
| 2 | FaxGesch | Office (Fax) |
| 3 | Mobil | Mobile (Phone) |
| 4 | MailGesch | Office (Email) |
| 5 | Internet | Website (Email) |
| 6 | TelPrivat | Home (Phone) |
| 7 | FaxPrivat | Home (Fax) |
| 8 | MailPrivat | Home (Email) |
| 9 | AutoTelefon | Other (Phone) |
| 10 | Sonstige | Other |
| 11 | EPost | EPost (Email) |
| 12 | Bea | BeA (Email) |

### Change Detection: Hash-basiert

**Problem**: Advoware Beteiligte-rowId ändert sich NICHT bei Kommunikations-Änderungen!

```python
# Beispiel
GET /api/v1/advonet/Beteiligte/104860
{
  "betNr": 104860,
  "rowId": "FBAB...",  # UNCHANGED!
  "kommunikation": [
    {"id": 88001, "rowId": "ABCD...", "tlf": "0511/12345"},
    {"id": 88002, "rowId": "EFGH...", "tlf": "max@example.com"}
  ]
}

# User ändert Email in Advoware → max@example.com zu new@example.com

GET /api/v1/advonet/Beteiligte/104860
{
  "betNr": 104860,
  "rowId": "FBAB...",  # STILL UNCHANGED!
  "kommunikation": [
    {"id": 88001, "rowId": "ABCD...", "tlf": "0511/12345"},
    {"id": 88002, "rowId": "IJKL...", "tlf": "new@example.com"}  # ← rowId CHANGED!
  ]
}
```

**Lösung**: MD5-Hash aller Kommunikation-rowIds

```python
# Hash-Berechnung
komm_rowids = sorted([k['rowId'] for k in kommunikationen])
komm_hash = hashlib.md5(''.join(komm_rowids).encode()).hexdigest()[:16]

# Speicherort in EspoCRM
cbeteiligte.kommunikationHash = "a3f5d2e8b1c4f6a9"  # 16 chars

# Vergleich
if stored_hash != current_hash:
    # Kommunikation hat sich geändert!
```

**Performance**: Hash nur neu berechnen wenn Advoware neu geladen wurde.

### Marker-Strategie für Bidirectional Matching

**Problem**: Wie matchen wir Einträge zwischen beiden Systemen?

```
EspoCRM:
- Email: max@example.com
- Phone: +49 511 12345

Advoware:
- tlf: "max@example.com", kommKz=4
- tlf: "+49 511 12345", kommKz=1

→ Wie wissen wir, dass EspoCRM-Email zu Advoware-ID 88002 gehört?
```

**Lösung**: Base64-Marker in Advoware `bemerkung`-Feld

```python
# Format
marker = f"[ESPOCRM:{base64_value}:{kommKz}]"

# Beispiel
bemerkung = "[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4]"
            # Base64 von "max@example.com", kommKz=4

# Decoding
import base64
decoded = base64.b64decode("bWF4QGV4YW1wbGUuY29t").decode()
# → "max@example.com"
```

**User-Bemerkungen werden preserviert**:
```python
bemerkung = "[ESPOCRM:...:4] Nur vormittags erreichbar"
            └─ Marker ─┘ └─ User-Text (bleibt erhalten!) ─┘
```

### 6 Sync-Varianten

Der Kommunikation-Sync behandelt **6 verschiedene Szenarien**:

| Var | Szenario | EspoCRM | Advoware | Aktion |
|-----|----------|---------|----------|--------|
| **Var1** | Neu in EspoCRM | +email | - | CREATE in Advoware (mit Marker) |
| **Var2** | Gelöscht in EspoCRM | -email | (marker) | CREATE Empty Slot |
| **Var3** | Gelöscht in Advoware | (entry) | -phone | DELETE in EspoCRM |
| **Var4** | Neu in Advoware | - | +phone | CREATE in EspoCRM (setze Marker) |
| **Var5** | Geändert in EspoCRM | email↑ | (synced) | UPDATE in Advoware |
| **Var6** | Geändert in Advoware | (synced) | phone↑ | UPDATE in EspoCRM (update Marker) |

#### Var1: Neu in EspoCRM → CREATE in Advoware

```python
# EspoCRM hat neue Email
espo_item = {
    'emailAddress': 'new@example.com',
    'type': 'Office'
}

# Mapper
komm_kz = 4  # MailGesch (Office Email)
base64_value = base64.b64encode(b'new@example.com').decode()
marker = f"[ESPOCRM:{base64_value}:4]"

# POST zu Advoware
POST /api/v1/advonet/Beteiligte/104860/Kommunikationen
{
  "tlf": "new@example.com",
  "kommKz": 4,
  "bemerkung": "[ESPOCRM:bmV3QGV4YW1wbGUuY29t:4]",
  "online": true
}

# Response
{
  "id": 88003,
  "rowId": "MNOP...",
  "tlf": "new@example.com",
  ...
}
```

#### Var2: Gelöscht in EspoCRM → Empty Slot

**Problem**: Advoware DELETE gibt 403!

**Lösung**: "Empty Slot" - Marker bleibt, aber tlf wird leer

```python
# User löscht Email in EspoCRM
# Advoware hat noch:
{
  "id": 88003,
  "tlf": "old@example.com",
  "bemerkung": "[ESPOCRM:b2xkQGV4YW1wbGUuY29t:4]"
}

# PUT zu Advoware
PUT /api/v1/advonet/Beteiligte/104860/Kommunikationen/88003
{
  "tlf": "",  # LEER!
  "bemerkung": "[ESPOCRM-SLOT:4]",  # Spezieller Slot-Marker
  "online": false
}

# Result: Slot kann später wiederverwendet werden
```

**Slot-Reuse**: Wenn User neue Email anlegt, wird Slot wiederverwendet:

```python
# EspoCRM: User legt neue Email an
espo_item = {'emailAddress': 'reuse@example.com', 'type': 'Office'}

# Sync findet Empty Slot mit kommKz=4
slot = next(k for k in advo_komm if k['bemerkung'] == '[ESPOCRM-SLOT:4]')

# PUT statt POST
PUT /api/v1/advonet/Beteiligte/104860/Kommunikationen/{slot.id}
{
  "tlf": "reuse@example.com",
  "bemerkung": "[ESPOCRM:cmV1c2VAZXhhbXBsZS5jb20=:4]",
  "online": true
}
```

#### Var3: Gelöscht in Advoware → DELETE in EspoCRM

```python
# Advoware: User löscht Telefonnummer
# EspoCRM hat noch:
espo_item = {
  'phoneNumber': '+49 511 12345',
  'type': 'Office'
}

# Sync erkennt: Marker fehlt in Advoware
# → DELETE in EspoCRM
DELETE /api/v1/espocrm/CBeteiligte/{id}/phoneNumbers/{phoneId}
```

#### Var4: Neu in Advoware → CREATE in EspoCRM

```python
# Advoware: User legt neue Telefonnummer an
advo_item = {
  "id": 88004,
  "tlf": "+49 30 987654",
  "kommKz": 1,
  "bemerkung": null  # KEIN Marker!
}

# Sync erkennt: Neuer Eintrag ohne Marker
# → CREATE in EspoCRM
POST /api/v1/espocrm/CBeteiligte/{id}/phoneNumbers
{
  "phoneNumber": "+49 30 987654",
  "type": "Office"
}

# → UPDATE Advoware (setze Marker)
PUT /api/v1/advonet/Beteiligte/104860/Kommunikationen/88004
{
  "tlf": "+49 30 987654",
  "bemerkung": "[ESPOCRM:KzQ5IDMwIDk4NzY1NA==:1]"
}
```

**Initial Sync Value-Matching**: Bei erstem Sync werden identische Werte NICHT doppelt angelegt:

```python
# Initial Sync (kein kommunikationHash in EspoCRM)
espo_items = [{'emailAddress': 'max@example.com'}]
advo_items = [{'tlf': 'max@example.com', 'bemerkung': null}]  # Kein Marker

# Ohne Value-Matching → 2x max@example.com!
# Mit Value-Matching → Nur Marker setzen, kein CREATE
if is_initial_sync and value in advo_values:
    # Nur Marker setzen
    PUT /api/v1/advonet/Beteiligte/.../Kommunikationen/{id}
    {
      "tlf": "max@example.com",
      "bemerkung": "[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4]"
    }
```

#### Var5: Geändert in EspoCRM → UPDATE in Advoware

```python
# User ändert Email in EspoCRM
old_value = "old@example.com"
new_value = "new@example.com"

# Advoware findet Entry via Marker
advo_item = find_by_marker(old_value, kommKz=4)

# UPDATE Advoware
PUT /api/v1/advonet/Beteiligte/.../Kommunikationen/{advo_item.id}
{
  "tlf": "new@example.com",
  "bemerkung": "[ESPOCRM:bmV3QGV4YW1wbGUuY29t:4]",  # Neuer Marker!
  "online": true
}
```

#### Var6: Geändert in Advoware → UPDATE in EspoCRM

```python
# User ändert Telefonnummer in Advoware
advo_item = {
  "id": 88001,
  "tlf": "+49 511 999999",  # GEÄNDERT!
  "bemerkung": "[ESPOCRM:KzQ5IDUxMSAxMjM0NQ==:1]"  # Alter Marker!
}

# Sync findet EspoCRM-Entry via Marker
espo_item = find_by_marker(decode_marker(bemerkung))

# UPDATE EspoCRM
PATCH /api/v1/espocrm/CBeteiligte/{id}/phoneNumbers/{espo_item.id}
{
  "phoneNumber": "+49 511 999999"
}

# UPDATE Advoware Marker
PUT /api/v1/advonet/Beteiligte/.../Kommunikationen/88001
{
  "tlf": "+49 511 999999",
  "bemerkung": "[ESPOCRM:KzQ5IDUxMSA5OTk5OTk=:1]"  # Neuer Marker!
}
```

### Konflikt-Behandlung

**Bei Beteiligte-Konflikt**: Kommunikation Sync mit `direction='to_advoware'`

```python
if beteiligte_conflict:
    # Kommunikation: Nur EspoCRM → Advoware
    await komm_sync.sync_bidirectional(
        entity_id, 
        betnr, 
        direction='to_advoware'  # ← WICHTIG!
    )
```

**Effekt**:
- ✅ Var1 (EspoCRM neu) → CREATE in Advoware
- ✅ Var2 (EspoCRM delete) → Empty Slot
- ✅ Var5 (EspoCRM change) → UPDATE Advoware
- ❌ Var3 (Advoware delete) → SKIP (nicht zu EspoCRM)
- ❌ Var4 (Advoware neu) → SKIP (nicht zu EspoCRM)
- ❌ Var6 (Advoware change) → REVERT (EspoCRM → Advoware)

**Var6-Revert**: Advoware-Änderungen werden rückgängig gemacht:

```python
# User änderte in Advoware: phone = "NEW"
# EspoCRM hat noch: phone = "OLD"

# Bei direction='to_advoware':
# → PUT zu Advoware mit "OLD" (Revert!)
PUT /api/v1/advonet/Beteiligte/.../Kommunikationen/{id}
{
  "tlf": "OLD",  # ← EspoCRM Value!
  "bemerkung": "[ESPOCRM:...:1]"
}
```

### Bekannte Einschränkungen

#### 1. Advoware kommKz ist READ-ONLY bei PUT

**Problem**: `kommKz` kann nach CREATE nicht mehr geändert werden

```python
# POST: kommKz=1 (TelGesch) - ✅ Funktioniert
POST /api/v1/advonet/Beteiligte/.../Kommunikationen
{"tlf": "...", "kommKz": 1}

# PUT: kommKz=3 (Mobil) - ❌ Wird ignoriert!
PUT /api/v1/advonet/Beteiligte/.../Kommunikationen/88001
{"tlf": "...", "kommKz": 3}  # ← Ignoriert, bleibt bei 1!
```

**Workaround**: Type-Änderungen erfordern DELETE + CREATE (aber DELETE gibt 403!)

**Praktische Lösung**: 
- User muss Type-Änderungen manuell in Advoware vornehmen
- Notification in EspoCRM: "Typ-Änderung nicht möglich, bitte manuell in Advoware"

#### 2. Advoware DELETE gibt 403

**Problem**: `/api/v1/advonet/Beteiligte/.../Kommunikationen/{id}` DELETE → 403

**Workaround**: Empty Slots (siehe Var2)

**Vorteil**: Slots können wiederverwendet werden, keine Duplikate

#### 3. Advoware GET kommKz=0 Bug

**Problem**: Bei GET sind alle `kommKz` Werte 0 (Advoware-Bug)

```python
# POST mit kommKz=4 ✅
Response: {"id": 88001, "kommKz": 4}

# GET ❌
Response: {"id": 88001, "kommKz": 0}  # Immer 0!
```

**Workaround**: kommKz aus EspoCRM + Marker ist "Source of Truth"

```python
# Marker enthält kommKz
marker = "[ESPOCRM:...:4]"
                      # ↑ kommKz=4
```

#### 4. Keine Änderungs-Detection für User-Bemerkungen

**Problem**: User-Bemerkungen werden nicht synchronisiert

```
Advoware: bemerkung = "[ESPOCRM:...:4] Nur vormittags"
EspoCRM: (keine Bemerkung)

→ User-Text "Nur vormittags" bleibt nur in Advoware
```

**Grund**: Marker + User-Text sind gemischt, Parsing komplex

**Workaround**: User-Text ist Advoware-spezifisch, wird nicht synced

#### 5. Performance: Sequentielle Verarbeitung

**Problem**: Var1-6 werden sequenziell verarbeitet (N API-Calls)

**Grund**: Advoware hat keinen Batch-Endpoint

**Auswirkung**: Bei 20 Änderungen = 20 API-Calls (dauert ~10 Sekunden)

**Workaround**: Lock-TTL ist 5 Minuten, reicht für normale Anzahl

---

## Sync Status Management

### Status-Werte

| Status | Gesetzt von | Bedeutung | Cron Action |
|--------|-------------|-----------|-------------|
| `pending_sync` | EspoCRM | Neu, wartet auf ersten Sync | ✅ Sync |
| `dirty` | EspoCRM | Geändert, Sync nötig | ✅ Sync |
| `syncing` | Python | Sync läuft (Lock aktiv) | ❌ Skip |
| `clean` | Python | Synchronisiert | ✅ Sync (nach 24h) |
| `failed` | Python | Fehler, Retry möglich | ✅ Retry mit Backoff |
| `permanently_failed` | Python | Max Retries (5x) | ✅ Auto-Reset nach 24h |
| `conflict` | Python | Konflikt (optional) | ✅ Sync |
| `deleted_in_advoware` | Python | 404 von Advoware | ❌ Skip |

### Status-Transitions

```
CREATE → pending_sync
         ↓ Webhook/Cron
         syncing
         ↓ Success
         clean
         
UPDATE → dirty
         ↓ Webhook/Cron
         syncing
         ↓ Success
         clean
         
Fehler → syncing
         ↓ Error
         failed (retry 1-4)
         ↓ Max Retries
         permanently_failed
         ↓ 24h später (Cron Auto-Reset)
         failed (retry 1)

Konflikt → syncing
           ↓ Conflict Detected
           conflict
           ↓ Webhook/Cron
           syncing (EspoCRM wins)
           ↓ Success
           clean
```

### Retry-Strategie mit Exponential Backoff

```python
RETRY_BACKOFF_MINUTES = [1, 5, 15, 60, 240]  # 1min, 5min, 15min, 1h, 4h
MAX_SYNC_RETRIES = 5

# Retry Logic
retry_count = espo_bet.get('syncRetryCount', 0)
backoff_minutes = RETRY_BACKOFF_MINUTES[retry_count]
next_retry = now + timedelta(minutes=backoff_minutes)

# Status Update
if retry_count >= MAX_SYNC_RETRIES:
    status = 'permanently_failed'
    auto_reset_at = now + timedelta(hours=24)
else:
    status = 'failed'
    next_retry_at = next_retry
```

**Cron Skip-Logik**:
```python
# Überspringe Entities wenn:
# 1. syncStatus='syncing' (Lock aktiv)
# 2. syncNextRetry > NOW (noch in Backoff-Phase)

if sync_next_retry and now < sync_next_retry:
    continue  # Skip this entity
```

### Lock-Management

**Redis Distributed Lock**:
```python
lock_key = f"sync_lock:cbeteiligte:{entity_id}"
acquired = redis.set(lock_key, "locked", nx=True, ex=300)  # 5min TTL
```

**Lock Release mit Nested try/finally**:
```python
lock_acquired = False
try:
    lock_acquired = await sync_utils.acquire_sync_lock(entity_id)
    if not lock_acquired:
        return  # Anderer Prozess synced bereits
    
    try:
        # Sync-Logik
        await sync_beteiligte(...)
        await sync_kommunikation(...)
        status = 'clean'
    except Exception as e:
        status = 'failed'
        raise
    finally:
        # GARANTIERTE Lock-Release (auch bei Exception)
        try:
            await sync_utils.release_sync_lock(entity_id, status, ...)
        except Exception as release_error:
            # Fallback: Force Redis lock delete
            await redis.delete(lock_key)
            
except Exception as outer_error:
    # Lock wurde nicht acquired, kein Release nötig
    pass
```

**Warum nested try/finally?**  
Garantiert Lock-Release auch bei kritischen Fehlern (Timeout, Connection Lost, Memory Error).

---

## Bekannte Einschränkungen

### Advoware API Limits

1. **DELETE gibt 403** für Beteiligte und Kommunikationen
   - Workaround: Soft-Delete mit Notification (Beteiligte) oder Empty Slots (Kommunikation)

2. **Nur 8 von 14 Beteiligte-Felder funktionieren**
   - Ignoriert: art, kurzname, geburtsname, familienstand, handelsRegisterNummer, registergericht
   - Workaround: Manuelle Pflege dieser Felder in Advoware

3. **kommKz ist READ-ONLY bei PUT**
   - Typ-Änderungen (z.B. TelGesch → Mobil) nicht möglich
   - Workaround: Notification, User ändert manuell

4. **kommKz=0 bei GET** (Advoware-Bug)
   - Workaround: Marker enthält korrekten kommKz-Wert

5. **Keine Batch-Endpoints**
   - N+1 Problem bei vielen Änderungen
   - Workaround: Sequentielle Verarbeitung mit Lock-TTL 5min

### System-Limits

1. **Max 100 Entities pro Cron-Run**
   - Grund: Timeout-Vermeidung
   - Workaround: Priorisierung (pending_sync > dirty > failed)

2. **Lock-TTL 5 Minuten**
   - Bei sehr vielen Kommunikations-Änderungen evtl. zu kurz
   - Workaround: Bei >50 Kommunikationen wird Lock erneuert

3. **Keine Transaktionen**
   - Partial Updates möglich bei Fehlern
   - Workaround: Hash wird nur bei vollständigem Erfolg updated

4. **Webhook-Race-Conditions**
   - Mehrere schnelle Updates können Race Conditions erzeugen
   - Workaround: Redis Lock + Cron-Fallback

---

## Troubleshooting

### Symptom: Entity bleibt bei 'syncing'

**Ursache**: Lock wurde nicht released (Server-Crash, Timeout)

**Lösung**:
```bash
# Redis Lock manuell löschen
redis-cli DEL sync_lock:cbeteiligte:68e4aef68d2b4fb98

# Entity Status zurücksetzen
# In EspoCRM Admin:
syncStatus = 'dirty'
```

### Symptom: Entity bei 'permanently_failed'

**Ursache**: 5 Sync-Versuche fehlgeschlagen

**Analyse**:
```python
# Check syncErrorMessage in EspoCRM
entity = await espocrm.get_entity('CBeteiligte', entity_id)
print(entity['syncErrorMessage'])
# z.B. "404 Not Found: betNr 104860"
```

**Lösungen**:
- **404**: betNr existiert nicht in Advoware → betNr in EspoCRM korrigieren
- **400**: Validation-Error → Daten prüfen (z.B. rechtsform nicht in Liste)
- **401**: Auth-Error → Advoware Token prüfen
- **503**: Advoware down → Warten auf Auto-Reset (24h)

**Manueller Reset**:
```python
# In EspoCRM Admin:
syncStatus = 'dirty'
syncRetryCount = 0
syncNextRetry = null
```

### Symptom: Duplikate in Kommunikation

**Ursache**: Initial Sync ohne Value-Matching (sollte gefixt sein)

**Analyse**:
```python
# Check kommunikationHash
entity = await espocrm.get_entity('CBeteiligte', entity_id)
print(entity['kommunikationHash'])  # null = Initial Sync lief nicht

# Check Marker in Advoware
advo_bet = await advoware.get_beteiligter(betnr)
for komm in advo_bet['kommunikation']:
    print(komm['bemerkung'])  # Sollte [ESPOCRM:...:X] enthalten
```

**Lösung**:
```python
# Manuell Duplikate entfernen (via EspoCRM)
# Dann syncStatus='dirty' → Re-Sync setzt Marker
```

### Symptom: Hash mismatch nach Sync

**Ursache**: Partial Update (einige Var fehlgeschlagen)

**Analyse**:
```python
# Check syncErrorMessage
entity = await espocrm.get_entity('CBeteiligte', entity_id)
print(entity['syncErrorMessage'])
# z.B. "Var1: 2 succeeded, Var4: 1 failed (403)"
```

**Lösung**:
- Hash wird NUR bei vollständigem Erfolg updated
- Bei partial failure: syncStatus='failed', Hash bleibt alt
- Retry wird Diff korrekt berechnen

### Symptom: Konflikt-Loop

**Ursache**: User ändert in Advoware während Sync läuft

**Lösung**: Lock sollte dies verhindern, aber bei Race Condition:
```python
# Check syncStatus History (in Logs)
# Wenn Konflikt > 3x in 15min → Manuelle Intervention

# Temporär Webhook disable für diese Entity
# In EspoCRM Admin:
syncStatus = 'clean'
# User kontaktieren, Änderungen koordinieren
```

---

## Best Practices

### 1. Monitoring

**Metriken**:
- `sync_duration_seconds` - Sync-Dauer pro Entity
- `sync_errors_total` - Anzahl Fehler pro Error-Type
- `sync_retries_total` - Retry-Count pro Entity
- `sync_conflicts_total` - Konflikt-Rate

**Alerts**:
- permanently_failed > 10 Entities
- sync_duration > 60 Sekunden
- sync_errors > 50/hour

### 2. Daten-Qualität

**Vor Sync prüfen**:
- ✅ rechtsform existiert in Advoware `/Rechtsformen`
- ✅ Email-Format valide
- ✅ Telefonnummer Format valide (E.164 empfohlen)

**Validation**:
```python
# In Event Handler vor Sync
validation_errors = await sync_utils.validate_entity(espo_entity)
if validation_errors:
    await sync_utils.release_sync_lock(
        entity_id, 
        'failed', 
        f"Validation failed: {validation_errors}"
    )
    return
```

### 3. Testing

**Test-Entities**:
- Dedizierte Test-betNr in Advoware (z.B. 999xxx)
- Test-Entities in EspoCRM mit Präfix "TEST-"
- Separate Service-Account für Tests

**Integration-Tests**:
```bash
# Scenario: Create-Update-Conflict
pytest tests/integration/test_beteiligte_sync.py::test_full_lifecycle
```

### 4. Rollback bei Problemen

**Webhook temporär disablen**:
```bash
# In EspoCRM Webhook-Settings:
# vmh.beteiligte.* → Status: Inactive
```

**Cron temporär stoppen**:
```bash
# In Kubernetes/systemd:
kubectl scale deployment motia-cron --replicas=0
```

**Entities zurücksetzen**:
```sql
-- In EspoCRM Database (PostgreSQL)
UPDATE c_beteiligte 
SET sync_status = 'pending_sync', 
    sync_retry_count = 0
WHERE sync_status = 'permanently_failed';
```

---

## Weiterführende Links

- [Beteiligte Sync Details](BETEILIGTE_SYNC.md)
- [Kommunikation Sync Details](KOMMUNIKATION_SYNC.md)
- [Field Mapping Referenz](ADVOWARE_BETEILIGTE_FIELDS.md)
- [Advoware API Swagger](advoware/advoware_api_swagger.json)
- [Troubleshooting Guide](TROUBLESHOOTING.md)
- [Archiv: Historische Analysen](archive/)
