# Kommunikation-Synchronisation: Analyse EspoCRM ↔ Advoware

**Erstellt**: 8. Februar 2026  
**Status**: ✅ API vollständig getestet  
**Basis**: Advoware API v1, EspoCRM Custom Entity

---

## 📋 Inhaltsverzeichnis

1. [Executive Summary](#executive-summary)
2. [Advoware API Analyse](#advoware-api-analyse)
3. [EspoCRM Konzept](#espocrm-konzept)
4. [Feld-Mapping](#feld-mapping)
5. [Sync-Strategie](#sync-strategie)
6. [Implementierungsplan](#implementierungsplan)

---

## 1. Executive Summary

### ✅ Was funktioniert

| Operation | Status | Felder |
|-----------|--------|--------|
| **POST** (Create) | ✅ Vollständig | Alle 4 Felder |
| **GET** (Read) | ✅ Vollständig | Enthalten in Beteiligte-Response |
| **PUT** (Update) | ⚠️ Teilweise | 3 von 4 Feldern |
| **DELETE** | ❌ 403 Forbidden | Nicht verfügbar |

### ⚠️ Kritische Einschränkungen

1. **kommKz ist READ-ONLY bei PUT**: Kommunikationstyp kann nach Erstellung nicht geändert werden
2. **Kein DELETE**: Manuelle Intervention via Notification erforderlich
3. **kommArt vs. kommKz**: `kommArt` wird automatisch von `kommKz` abgeleitet

---

## 2. Advoware API Analyse

### 2.1 Endpoints

```
POST   /api/v1/advonet/Beteiligte/{beteiligterId}/Kommunikationen
PUT    /api/v1/advonet/Beteiligte/{beteiligterId}/Kommunikationen/{kommunikationId}
GET    /api/v1/advonet/Beteiligte/{beteiligterId}  (enthält kommunikation array)
DELETE /api/v1/advonet/Beteiligte/{beteiligterId}/Kommunikationen/{kommunikationId}  ❌ 403
```

### 2.2 Datenmodell

#### POST/PUT Request Body

```json
{
  "tlf": "string (nullable)",
  "bemerkung": "string (nullable)",
  "kommKz": "integer (enum 1-12)",
  "online": "boolean"
}
```

#### Response (GET/POST/PUT)

```json
{
  "id": 88002,
  "betNr": 104860,
  "rowId": "FBABAAAANJFGABAAGJDOAEAPAAAAAPGFPDAFAAAA",
  "kommArt": 0,
  "kommKz": 1,
  "tlf": "0511/12345-60",
  "bemerkung": null,
  "online": false
}
```

### 2.3 KommKz Enum (Kommunikationskennzeichen)

| Wert | Name | Beschreibung |
|------|------|--------------|
| 1 | TelGesch | Geschäftstelefon |
| 2 | FaxGesch | Geschäftsfax |
| 3 | Mobil | Mobiltelefon |
| 4 | MailGesch | Geschäfts-Email |
| 5 | Internet | Website/URL |
| 6 | TelPrivat | Privattelefon |
| 7 | FaxPrivat | Privatfax |
| 8 | MailPrivat | Private Email |
| 9 | AutoTelefon | Autotelefon |
| 10 | Sonstige | Sonstige Kommunikation |
| 11 | EPost | E-Post (DE-Mail) |
| 12 | Bea | BeA (Besonderes elektronisches Anwaltspostfach) |

### 2.4 Feld-Analyse (✅ API-verifiziert)

#### ⚠️ KRITISCHER BUG: kommKz in GET immer 0

**Entdeckung**: Bei allen Tests zeigt der GET-Endpoint für **ALLE** Kommunikationen:
```json
{
  "kommKz": 0,
  "kommArt": 0
}
```

**Beobachtungen**:
1. ✅ POST Response: kommKz wird korrekt zurückgegeben (z.B. 3 für Mobil)
2. ✅ PUT Response: kommKz wird zurückgegeben (aber oft ignoriert bei Änderungsversuch)
3. ❌ GET Response: kommKz ist IMMER 0 (für alle Kommunikationen!)

**Test-Beispiel**:
```bash
POST mit kommKz=3 (Mobil)
→ POST Response: kommKz=3 ✓

GET nach POST
→ GET Response: kommKz=0 ✗

PUT mit kommKz=7 (Versuch zu ändern)
→ PUT Response: kommKz=3 (ignoriert!)
→ GET Response: kommKz=0 ✗
```

**Alle 11 getesteten Kommunikationen zeigen in GET: kommKz=0, kommArt=0**

**Mögliche Ursachen**:
1. Fehlende Berechtigung zum Lesen dieser Felder (Role-basiert)
2. Bug in Advoware GET-Serialisierung
3. kommKz wird nur intern gespeichert, nicht im Hauptdatensatz

**Implikationen für Sync**:
- ⚠️ kommKz kann NICHT via GET verifiziert werden
- ⚠️ Keine Möglichkeit den aktuellen Typ zu lesen
- ⚠️ Sync-Strategie muss angepasst werden: EspoCRM ist "Source of Truth"

#### POST (CREATE) - Alle Felder

```
✅ tlf          - string, nullable - Telefonnummer/Email/URL
✅ bemerkung    - string, nullable - Notiz/Beschreibung
✅ kommKz       - integer 1-12     - Kommunikationstyp
✅ online       - boolean          - Online-Kommunikation? (Email/Internet)
```

**Test-Ergebnis**: Alle 4 Felder können bei POST gesetzt werden.

#### PUT (UPDATE) - 3 von 4 Feldern

```
✅ tlf          - WRITABLE   - Kann geändert werden
✅ bemerkung    - WRITABLE   - Kann geändert werden
❌ kommKz       - READ-ONLY  - Kann NICHT geändert werden (bleibt beim Ursprungswert!)
✅ online       - WRITABLE   - Kann geändert werden
```

**Test-Ergebnis**: 
- `kommKz` wird bei PUT akzeptiert, aber ignoriert!
- Response enthält oft den ursprünglichen `kommKz`-Wert (aber nicht zuverlässig)
- **WICHTIG**: GET zeigt IMMER kommKz=0 (nicht nutzbar für Verifizierung!)
- `rowId` ändert sich bei jedem erfolgreichen PUT

#### Response-Only Felder (automatisch generiert)

```
🔒 id           - integer     - Kommunikations-ID (PK)
🔒 betNr        - integer     - Beteiligten-ID (FK)
🔒 rowId        - string      - Änderungserkennung (Base64, ~40 Zeichen)
🔒 kommArt      - integer     - Wird von kommKz abgeleitet
```

**Wichtig**: `kommArt` ist ein internes Feld, das Advoware automatisch aus `kommKz` berechnet.

### 2.5 Test-Ergebnisse

#### Test 1: POST - Neue Kommunikation erstellen ✅

```bash
POST /api/v1/advonet/Beteiligte/104860/Kommunikationen

Request:
{
  "kommKz": 1,
  "tlf": "+49 511 123456-10",
  "bemerkung": "TEST: Hauptnummer",
  "online": false
}

Response: 201 Created
[{
  "rowId": "FBABAAAAEMDGACAAFLBGNLBAAAAAEDNHOEAFAAAA",
  "id": 149331,
  "betNr": 104860,
  "kommArt": 1,
  "tlf": "+49 511 123456-10",
  "bemerkung": "TEST: Hauptnummer",
  "kommKz": 1,
  "online": false
}]
```

**Status**: ✅ Erfolgreich

#### Test 2: PUT - tlf ändern ✅

```bash
PUT /api/v1/advonet/Beteiligte/104860/Kommunikationen/149331

Request:
{
  "kommKz": 1,
  "tlf": "+49 511 999999-99",
  "bemerkung": "TEST: Hauptnummer",
  "online": false
}

Response: 200 OK
{
  "rowId": "FBABAAAAEMDGACAAFLBGNLBAAAAAEDNHOFABAAAA",  # GEÄNDERT!
  "id": 149331,
  "betNr": 104860,
  "kommArt": 1,
  "tlf": "+49 511 999999-99",  # GEÄNDERT!
  "bemerkung": "TEST: Hauptnummer",
  "kommKz": 1,
  "online": false
}
```

**Status**: ✅ tlf erfolgreich geändert, rowId aktualisiert

#### Test 3: PUT - kommKz ändern ❌

```bash
PUT /api/v1/advonet/Beteiligte/104860/Kommunikationen/149331

Request:
{
  "kommKz": 6,  # Versuche zu ändern: TelGesch → TelPrivat
  "tlf": "+49 511 999999-99",
  "bemerkung": "TEST: Geändert",
  "online": false
}

Response: 200 OK
{
  "rowId": "FBABAAAAEMDGACAAFLBGNLBAAAAAEDNHOFABAAAA",  # GEÄNDERT
  "id": 149331,
  "betNr": 104860,
  "kommArt": 1,
  "tlf": "+49 511 999999-99",
  "bemerkung": "TEST: Geändert",
  "kommKz": 1,  # NICHT GEÄNDERT! Bleibt bei 1
  "online": false
}
```

**Status**: ❌ kommKz wird IGNORIERT (bleibt bei 1), aber rowId ändert sich trotzdem

#### Test 4: DELETE ❌

```bash
DELETE /api/v1/advonet/Beteiligte/104860/Kommunikationen/149331

Response: 403 Forbidden
```

**Status**: ❌ DELETE nicht verfügbar (wie bei Adressen/Bankverbindungen)

---

## 3. EspoCRM Konzept

### 3.1 Aktuelle Situation (✅ API-verifiziert)

**Status**: ❌ **CKommunikation Entity existiert NICHT**

EspoCRM hat **KEINE** separate Kommunikations-Entity. Stattdessen:

#### Standard EspoCRM Felder in CBeteiligte

```json
{
  "id": "68e4af00172be7924",
  "name": "Max Mustermann",
  
  // Primäre Kommunikation (einfache Felder)
  "emailAddress": "max@example.com",
  "phoneNumber": "+49 511 12345",
  
  // Erweiterte Kommunikation (Arrays)
  "emailAddressData": [
    {
      "emailAddress": "max@example.com",
      "lower": "max@example.com",
      "primary": true,
      "optOut": false,
      "invalid": false
    },
    {
      "emailAddress": "max.private@gmail.com",
      "lower": "max.private@gmail.com",
      "primary": false,
      "optOut": false,
      "invalid": false
    }
  ],
  
  "phoneNumberData": [
    {
      "phoneNumber": "+49 511 12345",
      "primary": true,
      "type": "Office",
      "optOut": false,
      "invalid": false
    }
  ]
}
```

**Wichtige Erkenntnisse**:
- ❌ **Keine IDs** in emailAddressData/phoneNumberData
- ❌ **Kein Typ-Feld** (keine kommKz-Unterscheidung möglich)
- ✅ **primary Flag** für Haupt-Kommunikation
- ✅ **Arrays** unterstützen mehrere Einträge
- ⚠️ **NUR Email und Phone** - keine Fax, BeA, etc.

### 3.2 Zwei Sync-Strategien

#### Option A: Integration in Beteiligte-Sync (Einfach, eingeschränkt)

**Vorteile**:
- ✅ Keine neuen Entities erforderlich
- ✅ Nutzt vorhandene EspoCRM-Struktur
- ✅ Einfache Implementierung

**Nachteile**:
- ❌ Nur Email und Telefon (keine Fax, BeA, etc.)
- ❌ Kein Typ-Mapping (alle Emails sind "MailGesch", alle Phones sind "TelGesch")
- ❌ Kein Matching via ID möglich (nur via Wert)
- ❌ Schwierig zu synchronisieren (Array-Manipulation in Beteiligte-Update)

**Umsetzung**:
```python
# In beteiligte_sync.py nach Stammdaten-Update
await sync_kommunikation_from_espocrm_data(
    espo_entity['emailAddressData'],
    espo_entity['phoneNumberData'],
    betnr
)
```

#### Option B: Custom CKommunikation Entity (Empfohlen)

**Vorteile**:
- ✅ Vollständige Unterstützung aller 12 Advoware-Typen
- ✅ Separate Entity mit eigener ID (für Matching)
- ✅ Typ-Feld für kommKz-Mapping
- ✅ Saubere Trennung (separater Sync-Service)
- ✅ Flexibel erweiterbar

**Nachteile**:
- ⚠️ Custom Entity muss in EspoCRM angelegt werden
- ⚠️ Zusätzlicher Sync-Service erforderlich

**Entity-Design**:
```json
{
  "id": "string",
  "name": "string (auto-generiert)",
  "deleted": false,
  
  // Kommunikationsdaten
  "kommunikationstyp": "enum (kommKz)",
  "originalKommunikationstyp": "enum (IMMUTABLE nach CREATE)", 
  "wert": "string (tlf)",
  "bemerkung": "text",
  "isOnline": "bool",
  "isPrimary": "bool",
  
  // Beziehung
  "beteiligteId": "string (FK zu CBeteiligte)",
  "beteiligteName": "string (Link-Name)",
  
  // Advoware Sync
  "advowareId": "int",
  "advowareRowId": "varchar(50)",
  "syncStatus": "enum (clean|dirty|failed)",
  "advowareLastSync": "datetime",
  "syncErrorMessage": "text"
}
```

**Wichtig**: `originalKommunikationstyp` speichert den Typ bei Erstellung und ist IMMUTABLE.
Dies wird benötigt weil:
1. kommKz in Advoware GET nicht lesbar ist (Bug: immer 0)
2. kommKz in Advoware nicht änderbar ist (READ-ONLY)
3. EspoCRM muss als "Source of Truth" für den Typ dienen

### 3.3 Empfehlung

**➡️ Option B (Custom Entity) wird DRINGEND EMPFOHLEN** weil:

1. **Vollständigkeit**: Alle 12 Advoware-Typen unterstützt (nicht nur Email/Phone)
2. **Matching**: Entity-ID ermöglicht stabiles Matching
3. **Wartbarkeit**: Saubere Trennung von Stammdaten und Kommunikation
4. **Konsistenz**: Gleicher Ansatz wie Adressen und Bankverbindungen (separate Entities)

**Migration von Standard zu Custom**:
```python
# Einmaliger Import der bestehenden Daten
async def migrate_standard_to_custom():
    for bet in all_beteiligte:
        # Importiere Emails
        for email in bet['emailAddressData']:
            await espo.create_entity('CKommunikation', {
                'beteiligteId': bet['id'],
                'kommunikationstyp': 'MailGesch',
                'wert': email['emailAddress'],
                'isPrimary': email['primary'],
                'isOnline': True
            })
        
        # Importiere Phones
        for phone in bet['phoneNumberData']:
            await espo.create_entity('CKommunikation', {
                'beteiligteId': bet['id'],
                'kommunikationstyp': 'TelGesch',
                'wert': phone['phoneNumber'],
                'isPrimary': phone['primary'],
                'isOnline': False
            })
```

### 3.2 Kommunikationstyp Enum

```javascript
{
  "TelGesch": "Geschäftstelefon",
  "FaxGesch": "Geschäftsfax",
  "Mobil": "Mobiltelefon",
  "MailGesch": "Geschäfts-Email",
  "Internet": "Website",
  "TelPrivat": "Privattelefon",
  "FaxPrivat": "Privatfax",
  "MailPrivat": "Private Email",
  "AutoTelefon": "Autotelefon",
  "Sonstige": "Sonstige",
  "EPost": "E-Post",
  "Bea": "BeA"
}
```

### 3.3 Matching-Strategie

**Problem**: Keine stabile ID für Matching zwischen Systemen

**Lösungsansätze**:

1. **advowareId speichern** (bevorzugt)
   - Bei CREATE: Speichere `id` von Advoware Response
   - Bei SYNC: Matche via `advowareId`
   - ✅ Stabil, zuverlässig

2. **Kombination tlf + kommKz** (Fallback)
   - Matche via tlf-Wert UND Typ
   - ⚠️ Funktioniert nicht wenn tlf geändert wird
   - ⚠️ Duplikate möglich

**Empfehlung**: Variante 1 (advowareId) wie bei Adressen

---

## 4. Feld-Mapping

### 4.1 Kommunikationstypen-Mapping (Advoware → EspoCRM)

Da EspoCRM **keine separate CKommunikation Entity** hat, nutzen wir die Standard-Arrays:

| kommKz | Advoware Typ | EspoCRM Ziel | phoneNumberData.type | Notiz |
|--------|--------------|--------------|----------------------|-------|
| 1 | TelGesch | phoneNumberData | Office | ✅ |
| 2 | FaxGesch | phoneNumberData | Fax | ✅ |
| 3 | Mobil | phoneNumberData | Mobile | ✅ |
| 4 | MailGesch | emailAddressData | - | ✅ |
| 5 | Internet | ❌ **NICHT UNTERSTÜTZT** | - | URL-Feld fehlt |
| 6 | TelPrivat | phoneNumberData | Home | ✅ |
| 7 | FaxPrivat | phoneNumberData | Fax | ✅ |
| 8 | MailPrivat | emailAddressData | - | ✅ |
| 9 | AutoTelefon | phoneNumberData | Mobile | ✅ |
| 10 | Sonstige | phoneNumberData | Other | ✅ |
| 11 | EPost | emailAddressData | - | ✅ |
| 12 | Bea | emailAddressData | - | ✅ |

**11 von 12 Typen werden unterstützt** (nur "Internet" fehlt)

### 4.2 Advoware → EspoCRM Mapping

#### Email-Kommunikation (kommKz: 4, 8, 11, 12)

```python
# Advoware Kommunikation
{
  "id": 149331,
  "rowId": "eXqf+gAAAAAAAAA=",
  "kommKz": 4,          # MailGesch
  "kommArt": 1,         # Email
  "tlf": "max@example.com",
  "bemerkung": "Geschäftlich",
  "online": true
}

# → EspoCRM emailAddressData Element
{
  "emailAddress": "max@example.com",
  "lower": "max@example.com",
  "primary": true,      # Von Advoware (geschützt)
  "optOut": false,
  "invalid": false
}
```

**Mapping-Logik**:
- `tlf` → `emailAddress` und `lower`
- `online` → `primary` (Advoware-Einträge sind immer primary=true)
- `bemerkung` → ❌ Geht verloren (kein Feld in EspoCRM)

#### Phone-Kommunikation (kommKz: 1, 2, 3, 6, 7, 9, 10)

```python
# Advoware Kommunikation
{
  "id": 149332,
  "rowId": "eXqf+gAAAAAAAAB=",
  "kommKz": 3,          # Mobil
  "kommArt": 0,         # Telefon
  "tlf": "+49 170 1234567",
  "bemerkung": "Privat",
  "online": false
}

# → EspoCRM phoneNumberData Element
{
  "phoneNumber": "+49 170 1234567",
  "type": "Mobile",     # Von kommKz abgeleitet
  "primary": false,     # online=false
  "optOut": false,
  "invalid": false
}
```

**Typ-Mapping**:
```python
KOMMKZ_TO_PHONE_TYPE = {
    1: 'Office',     # TelGesch
    2: 'Fax',        # FaxGesch  
    3: 'Mobile',     # Mobil
    6: 'Home',       # TelPrivat
    7: 'Fax',        # FaxPrivat
    9: 'Mobile',     # AutoTelefon
    10: 'Other'      # Sonstige
}
```

### 4.3 Matching-Strategie: bemerkung-Marker System ✅ IMPLEMENTIERT

**Ausgangslage**:
- ❌ Separate CKommunikation Entity: Unpraktikabel
- ❌ PhoneNumber/EmailAddress Relationships: 403 Forbidden
- ❌ `id` Feld in emailAddressData: Wird ignoriert/entfernt
- ❌ kommKz/kommArt in GET: Beide immer 0 (Bug)
- ✅ Advoware hat eindeutige `id` pro Kommunikation
- ✅ Top-Level Felder (telGesch, emailGesch, etc.) für EINEN Eintrag pro Typ

**LÖSUNG: Marker in Advoware bemerkung-Feld**

#### Marker-Format:

```
[ESPOCRM:base64_value:kommKz] Optionale User-Bemerkung
[ESPOCRM-SLOT:kommKz]  (bei gelöschten Einträgen)
```

**Base64-Encoding**: Der Wert wird URL-safe Base64-kodiert gespeichert.

Beispiele:
- `[ESPOCRM:bWF4QGV4YW1wbGUuY29t:4] Hauptadresse` - Email von EspoCRM (max@example.com)
- `[ESPOCRM:KzQ5IDE3MCAxMjM0NTY3:1] Zentrale` - Telefon (+49 170 1234567)
- `[ESPOCRM-SLOT:3]` - Leerer Slot für Mobil (nach Löschung)
- `Wichtig: Nur vormittags` - Von Advoware (kein Marker)

**Warum Base64 statt Hash?**
```python
# Hash-Problem: Nicht rückrechenbar
old_hash = hash("old@example.com")  # abc123
new_value = "new@example.com"
# Kann old_hash nicht zu EspoCRM matchen!

# Base64-Lösung: Bidirektional
encoded = base64("old@example.com")  # b2xkQGV4YW1wbGUuY29t
decoded = decode(encoded)  # "old@example.com" ✅
# Kann dekodieren → Match in EspoCRM finden!
```

#### Typ-Erkennung (Priorität):

1. **Aus bemerkung-Marker** (wenn vorhanden) → Genau
2. **Aus Top-Level Feldern** (telGesch, emailGesch, etc.) → Genau für einen Eintrag
3. **Aus Wert** (Email='@', Phone=Rest) → Grob
4. **Default** (4=MailGesch, 1=TelGesch) → Fallback

```python
def detect_kommkz(wert: str, beteiligte: dict, bemerkung: str = None) -> int:
    """Erkenne kommKz mit mehrstufiger Strategie"""
    
    # 1. Aus Marker
    if bemerkung and '[ESPOCRM:' in bemerkung:
        match = re.search(r'\[ESPOCRM(?:-SLOT)?:[^:]+:(\d+)\]', bemerkung)
        if match:
            return int(match.group(1))
    
    # 2. Aus Top-Level Feldern (für EINEN Eintrag genau)
    type_map = {
        'telGesch': 1, 'faxGesch': 2, 'mobil': 3, 'emailGesch': 4,
        'internet': 5, 'telPrivat': 6, 'faxPrivat': 7, 'email': 4,
        'autotelefon': 9, 'ePost': 11, 'bea': 12
    }
    for field, kommkz in type_map.items():
        if beteiligte.get(field) == wert:
            return kommkz
    
    # 3. Aus Wert (Email vs. Phone)
    if '@' in wert:
        return 4  # MailGesch
    elif wert.strip():
        return 1  # TelGesch
    
    return 0
```

#### Bidirektionaler Sync - 4 Szenarien:

**Var1: Löschen in EspoCRM**
```python
# EspoCRM: max@example.com gelöscht
# Advoware: Eintrag mit "[ESPOCRM:abc:4] Geschäftlich"

# Sync erkennt: In Advoware aber nicht in EspoCRM
# → Leere Slot (Wert löschen, Typ behalten)
await advoware.api_call(f'.../Kommunikationen/{advo_id}', 'PUT', data={
    'tlf': '',
    'bemerkung': '[ESPOCRM-SLOT:4]',  # Slot-Marker
    'kommKz': 4,  # Bleibt
    'online': False
})
```

**Var2: Ändern in EspoCRM**
```python
# EspoCRM: max@old.com → max@new.com
# Advoware: "[ESPOCRM:old-hash:4]"

# Sync findet Eintrag via alten Hash
# → UPDATE mit neuem Wert
await advoware.api_call(f'.../Kommunikationen/{advo_id}', 'PUT', data={
    'tlf': 'max@new.com',
    'bemerkung': '[ESPOCRM:new-hash:4]',
    'kommKz': 4,
    'online': True
})
```

**Var3: Neu in EspoCRM**
```python
# EspoCRM: Neue Email hinzugefügt

# Sync sucht leeren Slot mit kommKz=4
empty_slots = [k for k in advo_komm 
               if '[ESPOCRM-SLOT:4]' in (k.get('bemerkung') or '')]

if empty_slots:
    # UPDATE leeren Slot
    await advoware.api_call(f'.../Kommunikationen/{slot_id}', 'PUT', ...)
else:
    # CREATE neue Kommunikation
    await advoware.api_call(f'.../Beteiligte/{betnr}/Kommunikationen', 'POST', ...)
```

**Var4: Neu in Advoware**
```python
# Advoware: Neue Kommunikation (keine Marker)

# Sync erkennt: Kein Marker in bemerkung
# → Neue Kommunikation von Advoware

# Typ-Erkennung:
kommkz = detect_kommkz(wert, beteiligte, bemerkung)  # Mit Top-Level

# Zu EspoCRM synchen + Marker setzen
await espo.update_entity('CBeteiligte', bet_id, {
    'emailAddressData': [...],  # Neue Email
})

# Marker in Advoware setzen
await advoware.api_call(f'.../Kommunikationen/{advo_id}', 'PUT', data={
    'tlf': wert,
    'bemerkung': f'[ESPOCRM:{hash}:{kommkz}] {original_bemerkung}',
    'kommKz': kommkz,
    'online': online
})
```

#### Vorteile:

| Vorteil | Beschreibung |
|---------|--------------|
| ✅ Bidirektional | CREATE/UPDATE/DELETE in beide Richtungen |
| ✅ Stabiles Matching | Via Hash in Marker |
| ✅ Typ-Erhaltung | kommKz wird gespeichert und wiederverwendet |
| ✅ Slot-Wiederverwendung | Gelöschte Einträge werden recycelt |
| ✅ Keine EspoCRM-Anpassung | Nutzt Standard emailAddressData/phoneNumberData |
| ✅ User-Bemerkung | Bleibt erhalten nach Marker |
| ✅ Minimaler Typ-Verlust | Top-Level Felder verbessern Typ-Erkennung |

#### Einschränkungen:

| Einschränkung | Impact | Mitigation |
|---------------|--------|-----------|
| ⚠️ Typ-Info teilweise verloren | Mehrere Telefone → alle TelGesch | Top-Level Matching minimiert Problem |
| ⚠️ bemerkung wird modifiziert | Marker im Feld sichtbar | Am Ende anfügen, prefix erkennbar |
| ⚠️ Leere Slots | Sammeln sich an | Periodischer Cleanup-Job |
| ⚠️ Hash-Kollisionen | Theoretisch möglich | SHA256[:8] = 1:16 Millionen |

---

## Option A: One-Way Sync (Advoware → EspoCRM) ⭐ EINFACHSTE LÖSUNG

**Prinzip**: Advoware ist Master, EspoCRM ist Read-Only Viewer

**Implementierung**:
```python
async def sync_kommunikation_one_way(betnr: int, bet_id: str):
    """
    Komplett-Überschreibung: Alle Kommunikationen von Advoware → EspoCRM
    
    Keine Change Detection, kein Matching - einfach überschreiben
    """
    # 1. Hole Advoware Kommunikationen
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]
    advo_komm = advo_data.get('kommunikation', [])
    
    # 2. Konvertiere ALLE zu EspoCRM Format
    emails = []
    phones = []
    
    for k in advo_komm:
        kommkz = k.get('kommKz', 0)
        wert = k.get('tlf', '').strip()
        if not wert:
            continue
        
        if kommkz in [4, 8, 11, 12]:  # Email-Typen
            emails.append({
                'emailAddress': wert,
                'lower': wert.lower(),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            })
        elif kommkz in [1, 2, 3, 6, 7, 9, 10]:  # Phone-Typen
            type_map = {1: 'Office', 2: 'Fax', 3: 'Mobile', 
                       6: 'Home', 7: 'Fax', 9: 'Mobile', 10: 'Other'}
            phones.append({
                'phoneNumber': wert,
                'type': type_map.get(kommkz, 'Other'),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            })
    
    # 3. KOMPLETT ÜBERSCHREIBEN (kein Merge!)
    await espo.update_entity('CBeteiligte', bet_id, {
        'emailAddressData': emails,
        'phoneNumberData': phones
    })
    
    context.logger.info(f"One-Way Sync: {len(emails)} emails, {len(phones)} phones")
```

**Vorteile**:
- ✅ Sehr einfach (50 Zeilen Code)
- ✅ Kein Matching nötig
- ✅ Keine Inkonsistenzen möglich
- ✅ Change Detection via Advoware rowId reicht

**Nachteile**:
- ❌ EspoCRM-Änderungen gehen verloren
- ❌ Nicht bidirektional
- ❌ User kann in EspoCRM nichts bearbeiten

**Geeignet wenn**:
- Advoware ist primäres System
- EspoCRM nur als Ansicht genutzt wird
- Keine Bearbeitung in EspoCRM gewünscht

---

## Option B: Wert-basiertes Matching mit Smart-Merge ⭐ BESTE BALANCE

**Prinzip**: Matching via emailAddress/phoneNumber + intelligentes Merging

**Implementierung**:
```python
async def sync_kommunikation_value_based(betnr: int, bet_id: str):
    """
    Wert-basiertes Matching mit Smart-Merge
    
    - Advoware-Einträge werden gematched und aktualisiert
    - EspoCRM-eigene Einträge bleiben erhalten
    - Bei Duplikaten: Advoware gewinnt
    """
    # 1. Hole beide Seiten
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]
    advo_komm = advo_data.get('kommunikation', [])
    
    espo_entity = await espo.get_entity('CBeteiligte', bet_id)
    espo_emails_current = espo_entity.get('emailAddressData', [])
    espo_phones_current = espo_entity.get('phoneNumberData', [])
    
    # 2. Konvertiere Advoware
    advo_emails = {}  # {emailAddress: data}
    advo_phones = {}  # {phoneNumber: data}
    
    for k in advo_komm:
        kommkz = k.get('kommKz', 0)
        wert = k.get('tlf', '').strip()
        if not wert:
            continue
        
        if kommkz in [4, 8, 11, 12]:
            advo_emails[wert] = {
                'emailAddress': wert,
                'lower': wert.lower(),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            }
        elif kommkz in [1, 2, 3, 6, 7, 9, 10]:
            type_map = {1: 'Office', 2: 'Fax', 3: 'Mobile', 
                       6: 'Home', 7: 'Fax', 9: 'Mobile', 10: 'Other'}
            advo_phones[wert] = {
                'phoneNumber': wert,
                'type': type_map.get(kommkz, 'Other'),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            }
    
    # 3. Smart-Merge: Advoware + nur nicht-existierende EspoCRM-Einträge
    merged_emails = list(advo_emails.values())
    merged_phones = list(advo_phones.values())
    
    # Füge EspoCRM-Einträge hinzu die NICHT in Advoware sind
    for espo_email in espo_emails_current:
        if espo_email['emailAddress'] not in advo_emails:
            merged_emails.append(espo_email)
    
    for espo_phone in espo_phones_current:
        if espo_phone['phoneNumber'] not in advo_phones:
            merged_phones.append(espo_phone)
    
    # 4. Update
    await espo.update_entity('CBeteiligte', bet_id, {
        'emailAddressData': merged_emails,
        'phoneNumberData': merged_phones
    })
    
    context.logger.info(
        f"Smart-Merge: {len(advo_emails)} Advoware emails, "
        f"{len(merged_emails) - len(advo_emails)} EspoCRM-only emails retained"
    )
```

**Vorteile**:
- ✅ Einfach zu implementieren (80 Zeilen)
- ✅ EspoCRM-eigene Einträge bleiben erhalten
- ✅ Teilweise bidirektional (neue Einträge von EspoCRM bleiben)
- ✅ Change Detection via rowId

**Nachteile**:
- ⚠️ EspoCRM-Änderungen an Advoware-Einträgen gehen verloren
- ⚠️ Bei Wert-Änderung in Advoware: Duplikat entsteht
- ⚠️ Kein echter bidirektionaler Sync

**Geeignet wenn**:
- Advoware ist primär, aber EspoCRM kann ergänzen
- User können in EspoCRM zusätzliche Kontakte hinzufügen
- Advoware-Einträge sollen nicht in EspoCRM geändert werden

---

## Option C: Array-Level Change Detection ⭐ FÜR KOMPLEXERE LOGIK

**Prinzip**: Speichere Hash des kompletten Arrays, bei Änderung: Analyse

**Implementierung**:
```python
import hashlib
import json

def calculate_array_hash(data: list) -> str:
    """Berechnet Hash für emailAddressData/phoneNumberData"""
    # Sortiere und normalisiere für stabilen Hash
    normalized = sorted([
        {k: v for k, v in item.items() if k != 'lower'}  # 'lower' ist redundant
        for item in data
    ], key=lambda x: x.get('emailAddress') or x.get('phoneNumber'))
    
    return hashlib.sha256(
        json.dumps(normalized, sort_keys=True).encode()
    ).hexdigest()[:16]


async def detect_kommunikation_changes(bet_id: str):
    """Erkennt ob emailAddressData/phoneNumberData geändert wurden"""
    
    # Hole aktuelle Daten
    entity = await espo.get_entity('CBeteiligte', bet_id)
    current_emails = entity.get('emailAddressData', [])
    current_phones = entity.get('phoneNumberData', [])
    
    # Berechne Hashes
    current_email_hash = calculate_array_hash(current_emails)
    current_phone_hash = calculate_array_hash(current_phones)
    
    # Hole gespeicherte Hashes aus Redis/DB
    stored_hashes = await get_kommunikation_hashes(bet_id)
    
    changes = {
        'emails_changed': current_email_hash != stored_hashes.get('email_hash'),
        'phones_changed': current_phone_hash != stored_hashes.get('phone_hash'),
        'current_email_hash': current_email_hash,
        'current_phone_hash': current_phone_hash
    }
    
    if changes['emails_changed'] or changes['phones_changed']:
        context.logger.info(f"Kommunikation changed for {bet_id}")
        
        # Analysiere WAS geändert wurde
        changes['added_emails'] = find_added_items(
            stored_hashes.get('emails', []), current_emails, 'emailAddress'
        )
        changes['removed_emails'] = find_removed_items(
            stored_hashes.get('emails', []), current_emails, 'emailAddress'
        )
        
        # Speichere neue Hashes
        await store_kommunikation_hashes(bet_id, {
            'email_hash': current_email_hash,
            'phone_hash': current_phone_hash,
            'emails': current_emails,
            'phones': current_phones
        })
    
    return changes


def find_added_items(old_list: list, new_list: list, key: str) -> list:
    """Findet hinzugefügte Einträge"""
    old_values = {item[key] for item in old_list}
    return [item for item in new_list if item[key] not in old_values]


def find_removed_items(old_list: list, new_list: list, key: str) -> list:
    """Findet entfernte Einträge"""
    new_values = {item[key] for item in new_list}
    return [item for item in old_list if item[key] not in new_values]
```

**Vorteile**:
- ✅ Erkennt granulare Änderungen (added/removed/modified)
- ✅ Kann intelligente Sync-Entscheidungen treffen
- ✅ Ermöglicht Konflikt-Handling

**Nachteile**:
- ⚠️ Komplexer (150+ Zeilen)
- ⚠️ Speichert Kopie der Daten (für Diff)
- ⚠️ Immer noch wert-basiertes Matching

**Geeignet wenn**:
- Granulare Change Detection gewünscht
- Konflikt-Handling wichtig
- Bereit für höhere Komplexität

---

## Empfehlung

**Für schnelle Implementation**: ✅ **Option A** (One-Way Sync)
- 50 Zeilen Code
- In 1 Stunde implementiert
- Deckt 80% der Use-Cases ab

**Für Produktiv-Einsatz**: ✅ **Option B** (Smart-Merge)
- 80 Zeilen Code  
- Beste Balance zwischen Einfachheit und Flexibilität
- EspoCRM-User können ergänzen

#### Struktur des Custom Fields

```json
{
  "kommunikationMapping": {
    "emails": [
      {
        "emailAddress": "max@example.com",
        "advowareId": 149331,
        "advowareRowId": "eXqf+gAAAAAAAAA=",
        "lastSync": "2026-02-08T10:30:00Z"
      },
      {
        "emailAddress": "info@company.com",
        "advowareId": 149332,
        "advowareRowId": "eXqf+gAAAAAAAAB=",
        "lastSync": "2026-02-08T10:30:00Z"
      }
    ],
    "phones": [
      {
        "phoneNumber": "+49 511 12345",
        "advowareId": 149333,
        "advowareRowId": "eXqf+gAAAAAAAAC=",
        "lastSync": "2026-02-08T10:30:00Z"
      }
    ]
  }
}
```

#### EspoCRM Custom Field Konfiguration

**Feld-Definition** (in EspoCRM Admin → Entity Manager → CBeteiligte → Fields):
- **Name**: `kommunikationMapping`
- **Type**: `Text` (oder `Wysiwyg` falls UI wichtig)
- **Label**: `Kommunikation Sync Mapping` (wird nicht im UI angezeigt)
- **Tooltip**: `Mapping von Advoware Kommunikations-IDs (automatisch verwaltet)`
- **Read-Only**: ✅ Yes (User soll nicht editieren)
- **Hidden in Detail**: ✅ Yes (nicht sichtbar)

#### Matching-Algorithmus

```python
async def match_email_with_advoware(email_address: str, bet_id: str) -> Optional[dict]:
    """
    Findet Advoware-Kommunikation für eine Email-Adresse
    
    Returns: {"advowareId": 123, "advowareRowId": "ABC"} oder None
    """
    # Hole Mapping aus EspoCRM
    entity = await espo.get_entity('CBeteiligte', bet_id)
    mapping_json = entity.get('kommunikationMapping')
    
    if not mapping_json:
        return None
    
    mapping = json.loads(mapping_json) if isinstance(mapping_json, str) else mapping_json
    
    # Suche Email
    for email_entry in mapping.get('emails', []):
        if email_entry['emailAddress'] == email_address:
            return {
                'advowareId': email_entry['advowareId'],
                'advowareRowId': email_entry['advowareRowId']
            }
    
    return None


async def update_kommunikation_mapping(bet_id: str, betnr: int):
    """
    Aktualisiert das Mapping basierend auf aktuellen Advoware-Daten
    
    Wird aufgerufen:
    - Nach jedem Advoware → EspoCRM Sync
    - Bei Beteiligte-Webhook
    """
    # Hole Advoware Kommunikationen
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]
    advo_komm = advo_data.get('kommunikation', [])
    
    # Baue Mapping
    mapping = {
        'emails': [],
        'phones': []
    }
    
    for k in advo_komm:
        kommkz = k.get('kommKz', 0)
        wert = k.get('tlf', '').strip()
        if not wert:
            continue
        
        entry = {
            'advowareId': k.get('id'),
            'advowareRowId': k.get('rowId'),
            'lastSync': datetime.now().isoformat()
        }
        
        # Email-Typen
        if kommkz in [4, 8, 11, 12]:
            entry['emailAddress'] = wert
            mapping['emails'].append(entry)
        
        # Phone-Typen
        elif kommkz in [1, 2, 3, 6, 7, 9, 10]:
            entry['phoneNumber'] = wert
            mapping['phones'].append(entry)
    
    # Speichere Mapping
    await espo.update_entity('CBeteiligte', bet_id, {
        'kommunikationMapping': json.dumps(mapping, ensure_ascii=False)
    })
```

#### Sync-Ablauf mit Mapping

**Advoware → EspoCRM** (Webhook-getriggert):

```python
async def sync_kommunikation_from_advoware(betnr: int, bet_id: str):
    """Vollständiger Sync mit Mapping-Update"""
    
    # 1. Hole Advoware Daten
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]
    advo_komm = advo_data.get('kommunikation', [])
    
    # 2. Konvertiere zu EspoCRM Format
    emails = []
    phones = []
    mapping = {'emails': [], 'phones': []}
    
    for k in advo_komm:
        kommkz = k.get('kommKz', 0)
        wert = k.get('tlf', '').strip()
        if not wert:
            continue
        
        # Email
        if kommkz in [4, 8, 11, 12]:
            emails.append({
                'emailAddress': wert,
                'lower': wert.lower(),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            })
            mapping['emails'].append({
                'emailAddress': wert,
                'advowareId': k.get('id'),
                'advowareRowId': k.get('rowId'),
                'lastSync': datetime.now().isoformat()
            })
        
        # Phone
        elif kommkz in [1, 2, 3, 6, 7, 9, 10]:
            type_map = {1: 'Office', 2: 'Fax', 3: 'Mobile', 
                       6: 'Home', 7: 'Fax', 9: 'Mobile', 10: 'Other'}
            phones.append({
                'phoneNumber': wert,
                'type': type_map.get(kommkz, 'Other'),
                'primary': k.get('online', False),
                'optOut': False,
                'invalid': False
            })
            mapping['phones'].append({
                'phoneNumber': wert,
                'advowareId': k.get('id'),
                'advowareRowId': k.get('rowId'),
                'lastSync': datetime.now().isoformat()
            })
    
    # 3. Update EspoCRM (Daten + Mapping)
    await espo.update_entity('CBeteiligte', bet_id, {
        'emailAddressData': emails,
        'phoneNumberData': phones,
        'kommunikationMapping': json.dumps(mapping, ensure_ascii=False)
    })
```

**EspoCRM → Advoware** (Change Detection):

```python
async def sync_kommunikation_to_advoware(bet_id: str, betnr: int):
    """
    Synchronisiert Änderungen von EspoCRM zu Advoware
    
    Wird aufgerufen bei:
    - EspoCRM-Webhook (CBeteiligte UPDATE)
    - Change Detection erkennt emailAddressData/phoneNumberData Änderung
    """
    # Hole EspoCRM Daten
    entity = await espo.get_entity('CBeteiligte', bet_id)
    current_emails = entity.get('emailAddressData', [])
    current_phones = entity.get('phoneNumberData', [])
    
    # Hole Mapping
    mapping_json = entity.get('kommunikationMapping', '{}')
    mapping = json.loads(mapping_json) if isinstance(mapping_json, str) else mapping_json
    
    # Verarbeite Emails
    for email in current_emails:
        email_addr = email['emailAddress']
        
        # Finde im Mapping
        advo_info = next((e for e in mapping.get('emails', []) 
                         if e['emailAddress'] == email_addr), None)
        
        if advo_info:
            # UPDATE in Advoware
            advo_id = advo_info['advowareId']
            await advoware.api_call(
                f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{advo_id}',
                method='PUT',
                data={
                    'kommKz': 4,  # Von gespeichertem Typ (via separate Logik)
                    'tlf': email_addr,
                    'bemerkung': '',
                    'online': email.get('primary', False)
                }
            )
        else:
            # CREATE in Advoware (neue Email)
            result = await advoware.api_call(
                f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen',
                method='POST',
                data={
                    'kommKz': 4,  # MailGesch
                    'tlf': email_addr,
                    'bemerkung': 'Von EspoCRM erstellt',
                    'online': email.get('primary', False)
                }
            )
            
            # Update Mapping
            created = result[0] if isinstance(result, list) else result
            mapping.setdefault('emails', []).append({
                'emailAddress': email_addr,
                'advowareId': created['id'],
                'advowareRowId': created['rowId'],
                'lastSync': datetime.now().isoformat()
            })
    
    # Erkenne GELÖSCHTE Emails (in Mapping aber nicht in current_emails)
    current_email_addrs = {e['emailAddress'] for e in current_emails}
    for mapped_email in mapping.get('emails', []):
        if mapped_email['emailAddress'] not in current_email_addrs:
            # Email wurde in EspoCRM gelöscht → DELETE in Advoware
            advo_id = mapped_email['advowareId']
            try:
                await advoware.api_call(
                    f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{advo_id}',
                    method='DELETE'
                )
            except Exception as e:
                if '403' in str(e):
                    # DELETE nicht erlaubt → Notification
                    await notification_manager.notify_manual_action_required(
                        entity_type='CBeteiligte',
                        entity_id=bet_id,
                        action_type='delete_not_supported',
                        details={
                            'message': 'Kommunikation kann nicht gelöscht werden',
                            'advoware_id': advo_id,
                            'email': mapped_email['emailAddress']
                        }
                    )
    
    # Update Mapping
    await espo.update_entity('CBeteiligte', bet_id, {
        'kommunikationMapping': json.dumps(mapping, ensure_ascii=False)
    })
```

#### Vorteile der Custom Field Lösung

| Aspekt | Lösung |
|--------|---------|
| **Stabiles Matching** | ✅ Via advowareId (nicht abhängig vom Wert) |
| **Change Detection** | ✅ Via advowareRowId |
| **Bidirektional** | ✅ Vollständig (CREATE/UPDATE/DELETE) |
| **Wert-Änderungen** | ✅ Kein Problem (Matching via ID) |
| **DELETE Detection** | ✅ Möglich (Vergleich Mapping vs. current) |
| **Typ-Tracking** | ✅ Via separates Feld oder Ableitung |
| **Implementation** | ⚠️ Erfordert Custom Field in EspoCRM |

#### Nachteile & Mitigations

| Nachteil | Mitigation |
|----------|-----------|
| Custom Field nötig | Einmaliges Setup in EspoCRM Admin |
| Daten-Duplikation | Akzeptabel (Mapping ist klein) |
| Inkonsistenz möglich | Auto-Rebuild bei jedem Advoware-Sync |
| User könnte löschen | Field als readOnly + hidden markieren |

### 4.4 Alternative: Wert-basiertes Matching (Fallback)

Falls Custom Field NICHT gewünscht, gibt es einen einfacheren Ansatz ohne IDs:

**Hybrid-Strategie ohne Mapping**:
- Matching via `emailAddress`/`phoneNumber` Wert
- Bei Wert-Änderung: DELETE + CREATE (kein UPDATE)
- Keine DELETE-Detection möglich
- Nur One-Way: Advoware → EspoCRM

Siehe [Abschnitt 5.1](#51-advoware--espocrm-webhook-getriggert) für Details.

#### 1. Advoware → EspoCRM (primary=true)

Alle Advoware-Kommunikationen werden mit **primary=true** markiert (geschützt):

```python
# Sync-Ablauf
advoware_emails = get_advoware_kommunikation(betnr, types=[4, 8, 11, 12])
espocrm_emails_current = get_espocrm_entity(bet_id)['emailAddressData']

# Trenne primary (Advoware) von non-primary (EspoCRM-only)
espocrm_secondary = [e for e in espocrm_emails_current if not e.get('primary')]

# Konvertiere Advoware zu EspoCRM Format
advoware_as_espocrm = [
    {
        'emailAddress': k['tlf'],
        'lower': k['tlf'].lower(),
        'primary': True,  # IMMER true für Advoware
        'optOut': False,
        'invalid': False
    }
    for k in advoware_emails
]

# Merge: Advoware (primary) + EspoCRM (secondary)
merged = advoware_as_espocrm + espocrm_secondary

# Update CBeteiligte
await espo.update_entity('CBeteiligte', bet_id, {
    'emailAddressData': merged
})
```

**Vorteile**:
- ✅ Advoware behält vollständige Kontrolle
- ✅ EspoCRM kann eigene Einträge ergänzen (primary=false)
- ✅ Kein Datenverlust
- ✅ Nutzt Standard-EspoCRM-Felder

#### 2. EspoCRM → Advoware (NUR primary=false)

Nur EspoCRM-eigene Einträge (primary=false) werden **NICHT** zu Advoware synchronisiert:

```python
# Bei EspoCRM-Webhook: Prüfe primary-Flag
for email in espocrm_entity['emailAddressData']:
    if email.get('primary'):
        # Von Advoware → IGNORIEREN (wird via Advoware-Webhook synchronisiert)
        continue
    else:
        # EspoCRM-eigener Eintrag → Behalten (nur in EspoCRM)
        pass
```

#### 3. Change Detection

- **Advoware**: Via `rowId` (wie bei Adressen/Bankverbindungen)
- **EspoCRM**: Keine Change Detection für primary=false Einträge
- **Advoware ist Master** für alle primary=true Einträge

#### 4. Wert-Änderungen (Edge Case)

**Szenario**: Email/Phone ändert in Advoware

```
Vorher:  max@old.com  (Advoware ID=123, rowId=ABC)
Nachher: max@new.com  (Advoware ID=123, rowId=XYZ)  # rowId ändert!
```

**Problem**: Matching via Wert findet `max@old.com` nicht mehr

**Verhalten**:
1. Sync erkennt rowId-Änderung von Advoware-Eintrag 123
2. Sucht `max@new.com` in EspoCRM → nicht gefunden
3. Fügt `max@new.com` mit primary=true hinzu
4. `max@old.com` bleibt mit primary=false erhalten (!) 

**Ergebnis**: Temporäres Duplikat

**Cleanup**: 
- Option A: User löscht manuell in EspoCRM
- Option B: Automatisches Cleanup von verwaisten primary=false Einträgen mit alten Advoware-Pattern

### 4.4 Akzeptierte Einschränkungen

| Einschränkung | Impact | Mitigation |
|---------------|--------|-----------|
| ❌ Kein ID-Feld | Matching via Wert fragil | primary-Flag trennt Advoware/EspoCRM |
| ❌ Wert-Änderung → Duplikat | User sieht alte+neue Adresse | Manueller Cleanup oder Auto-Cleanup-Job |
| ❌ bemerkung geht verloren | Notizen nicht in EspoCRM | Akzeptiert (EspoCRM hat kein Feld) |
| ❌ kommKz unlesbar (Bug) | Typ-Info verloren | Irrelevant (Typ ergibt sich aus Array) |
| ❌ Internet-Typ fehlt | URLs nicht sync-bar | Akzeptiert (11/12 Typen OK) |

### 4.5 EspoCRM → Advoware Mapping (Optional)

```python
{
    'kommKz': map_enum(espo['kommunikationstyp']),  # 1-12
    'tlf': espo['wert'],                            # "+49 511..."
    'bemerkung': espo['bemerkung'],                 # Notiz
    'online': espo['isOnline']                      # Boolean
}
```

**Enum-Mapping**:

```python
ESPO_TO_ADVO_KOMMKZ = {
    'TelGesch': 1,
    'FaxGesch': 2,
    'Mobil': 3,
    'MailGesch': 4,
    'Internet': 5,
    'TelPrivat': 6,
    'FaxPrivat': 7,
    'MailPrivat': 8,
    'AutoTelefon': 9,
    'Sonstige': 10,
    'EPost': 11,
    'Bea': 12
}
```

### 4.2 EspoCRM → Advoware (UPDATE)

```python
{
    # kommKz NICHT ÄNDERBAR!
    'kommKz': current_advo['kommKz'],  # Verwende aktuellen Wert
    'tlf': espo['wert'],               # ÄNDERBAR
    'bemerkung': espo['bemerkung'],    # ÄNDERBAR
    'online': espo['isOnline']         # ÄNDERBAR
}
```

**Wichtig**: 
- `kommKz` MUSS im Request enthalten sein (API-Validierung)
- Aber Wert wird ignoriert - immer aktuellen Wert verwenden!

### 4.3 Advoware → EspoCRM

```python
{
    'name': f"{map_enum_reverse(advo['kommKz'])}: {advo['tlf'][:30]}",
    'kommunikationstyp': map_enum_reverse(advo['kommKz']),
    'wert': advo['tlf'],
    'bemerkung': advo['bemerkung'],
    'isOnline': advo['online'],
    'advowareId': advo['id'],
    'advowareRowId': advo['rowId']
}
```

**Enum-Mapping (Reverse)**:

```python
ADVO_TO_ESPO_KOMMKZ = {
    1: 'TelGesch',
    2: 'FaxGesch',
    3: 'Mobil',
    4: 'MailGesch',
    5: 'Internet',
    6: 'TelPrivat',
    7: 'FaxPrivat',
    8: 'MailPrivat',
    9: 'AutoTelefon',
    10: 'Sonstige',
    11: 'EPost',
    12: 'Bea'
}
```

### 4.4 READ-ONLY Felder Detection

```python
def detect_readonly_changes(espo_entity, advo_entity):
    """Prüft ob READ-ONLY Felder geändert wurden"""
    
    espo_kommkz = ESPO_TO_ADVO_KOMMKZ.get(espo_entity['kommunikationstyp'])
    advo_kommkz = advo_entity['kommKz']
    
    if espo_kommkz != advo_kommkz:
        return {
            'readonly_fields': ['kommunikationstyp'],
            'espo_value': espo_entity['kommunikationstyp'],
            'advo_value': ADVO_TO_ESPO_KOMMKZ[advo_kommkz]
        }
    
    return None
```

---

## 5. Sync-Strategie

**Entscheidung**: Integration in Beteiligte-Sync (kein separates CKommunikation Entity)

### 5.1 Advoware → EspoCRM (Webhook-getriggert)

```python
async def sync_kommunikation_to_espocrm(betnr: int, bet_id: str):
    """
    Synchronisiert Advoware Kommunikationen zu EspoCRM als Teil von CBeteiligte
    
    Wird getriggert von:
    - Beteiligte-Webhook (wenn rowId von Kommunikationen ändert)
    - Kann auch manuell aufgerufen werden
    """
    
    # 1. Hole Advoware Beteiligte (inkl. Kommunikationen)
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]  # API gibt Liste zurück
    advo_komm = advo_data.get('kommunikation', [])
    
    # 2. Hole aktuelle EspoCRM emailAddressData/phoneNumberData
    espo_entity = await espocrm.get_entity('CBeteiligte', bet_id)
    espo_emails_current = espo_entity.get('emailAddressData', [])
    espo_phones_current = espo_entity.get('phoneNumberData', [])
    
    # 3. Konvertiere Advoware zu EspoCRM Format
    advo_as_emails = []
    advo_as_phones = []
    
    for k in advo_komm:
        kommkz = k.get('kommKz', 0)
        wert = k.get('tlf', '').strip()
        if not wert:
            continue  # Skip leere Einträge
        
        # Email-Typen: 4=MailGesch, 8=MailPrivat, 11=EPost, 12=Bea
        if kommkz in [4, 8, 11, 12]:
            advo_as_emails.append({
                'emailAddress': wert,
                'lower': wert.lower(),
                'primary': True,  # Markiere als Advoware-Eintrag
                'optOut': False,
                'invalid': False
            })
        
        # Phone-Typen: 1,2,3,6,7,9,10 (alle außer 4,5,8,11,12)
        elif kommkz in [1, 2, 3, 6, 7, 9, 10]:
            type_map = {
                1: 'Office',    # TelGesch
                2: 'Fax',       # FaxGesch
                3: 'Mobile',    # Mobil
                6: 'Home',      # TelPrivat
                7: 'Fax',       # FaxPrivat
                9: 'Mobile',    # AutoTelefon
                10: 'Other'     # Sonstige
            }
            advo_as_phones.append({
                'phoneNumber': wert,
                'type': type_map.get(kommkz, 'Other'),
                'primary': True,  # Markiere als Advoware-Eintrag
                'optOut': False,
                'invalid': False
            })
        # kommKz=5 (Internet) wird übersprungen (nicht unterstützt)
    
    # 4. Behalte EspoCRM-eigene Einträge (primary=false)
    espo_secondary_emails = [e for e in espo_emails_current if not e.get('primary', False)]
    espo_secondary_phones = [p for p in espo_phones_current if not p.get('primary', False)]
    
    # 5. Merge: Advoware (primary) + EspoCRM (secondary)
    merged_emails = advo_as_emails + espo_secondary_emails
    merged_phones = advo_as_phones + espo_secondary_phones
    
    # 6. Update CBeteiligte
    update_data = {
        'emailAddressData': merged_emails,
        'phoneNumberData': merged_phones
    }
    
    await espocrm.update_entity('CBeteiligte', bet_id, update_data)
    
    context.logger.info(
        f"Kommunikation synced: {len(advo_as_emails)} emails, "
        f"{len(advo_as_phones)} phones from Advoware + "
        f"{len(espo_secondary_emails)} EspoCRM emails, "
        f"{len(espo_secondary_phones)} EspoCRM phones"
    )
```

**Wichtig**: 
- Alle Advoware-Einträge haben `primary=true`
- EspoCRM-eigene Einträge haben `primary=false` und bleiben erhalten
- Bei jedem Sync werden Advoware-Einträge komplett überschrieben

### 5.2 Change Detection

```python
async def handle_beteiligte_webhook(webhook_data):
    """
    Webhook von Advoware bei Beteiligte-Änderung
    
    Prüft ob Kommunikationen geändert wurden via rowId
    """
    
    betnr = webhook_data['beteiligterId']
    
    # Hole aktuelle Advoware-Daten
    advo_entity = await advoware.api_call(f'api/v1/advonet/Beteiligte/{betnr}')
    advo_data = advo_entity[0]
    advo_komm = advo_data.get('kommunikation', [])
    
    # Hole gespeicherte rowIds aus Redis/DB
    stored_row_ids = await get_stored_kommunikation_rowids(betnr)
    current_row_ids = [k.get('rowId') for k in advo_komm if k.get('rowId')]
    
    # Vergleiche
    if set(current_row_ids) != set(stored_row_ids):
        context.logger.info(f"Kommunikation changed for BetNr {betnr}")
        
        # Sync zu EspoCRM
        bet_id = await get_espocrm_id_for_betnr(betnr)
        await sync_kommunikation_to_espocrm(betnr, bet_id)
        
        # Update gespeicherte rowIds
        await store_kommunikation_rowids(betnr, current_row_ids)
    else:
        context.logger.debug(f"No kommunikation changes for BetNr {betnr}")
```

### 5.3 EspoCRM → Advoware (Optional, nicht empfohlen)

**Entscheidung**: EspoCRM-eigene Einträge (primary=false) werden **NICHT** zu Advoware synchronisiert.

**Begründung**:
- EspoCRM kann keine Advoware-IDs speichern (kein custom field in Arrays)
- Matching via Wert ist fragil (bei Änderung)
- Konflikt-Handling komplex
- User-Story: EspoCRM als "Viewer" mit optionalen Ergänzungen

**Alternative** (falls gewünscht): One-Shot-Import

```python
async def import_espocrm_kommunikation_to_advoware(bet_id: str, betnr: int):
    """
    Einmaliger Import von EspoCRM → Advoware
    
    NUR für primary=false Einträge (EspoCRM-eigene)
    User muss manuell triggern
    """
    
    espo_entity = await espocrm.get_entity('CBeteiligte', bet_id)
    
    # Nur non-primary Einträge
    to_import_emails = [e for e in espo_entity.get('emailAddressData', []) 
                        if not e.get('primary', False)]
    to_import_phones = [p for p in espo_entity.get('phoneNumberData', []) 
                        if not p.get('primary', False)]
    
    for email in to_import_emails:
        # Erstelle in Advoware
        await advoware.api_call(
            f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen',
            method='POST',
            data={
                'kommKz': 4,  # MailGesch
                'tlf': email['emailAddress'],
                'bemerkung': 'Importiert aus EspoCRM',
                'online': email.get('primary', False)
            }
        )
    
    # Danach: Setze primary=true (jetzt von Advoware kontrolliert)
    await resync_kommunikation_to_espocrm(betnr, bet_id)
```

```python
async def create_kommunikation(espo_entity, betnr):
    """Erstellt neue Kommunikation in Advoware"""
    
    # 1. Mappe ALLE Felder
    advo_data = {
        'kommKz': ESPO_TO_ADVO_KOMMKZ[espo_entity['kommunikationstyp']],
        'tlf': espo_entity['wert'],
        'bemerkung': espo_entity['bemerkung'],
        'online': espo_entity['isOnline']
    }
    
    # 2. POST zu Advoware
    result = await advoware.api_call(
        f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen',
        method='POST',
        data=advo_data
    )
    
    # 3. Extrahiere ID und rowId
    created = result[0] if isinstance(result, list) else result
    
    # 4. Update EspoCRM mit Advoware-IDs
    await espocrm.update_entity('CKommunikation', espo_entity['id'], {
        'advowareId': created['id'],
        'advowareRowId': created['rowId'],
        'syncStatus': 'clean',
        'advowareLastSync': datetime.now()
    })
```

### 5.2 UPDATE (EspoCRM → Advoware)

```python
async def update_kommunikation(espo_entity, betnr):
    """Update Kommunikation (nur R/W Felder)"""
    
    advoware_id = espo_entity['advowareId']
    
    # WICHTIG: kommKz kann NICHT via GET gelesen werden (Bug: immer 0)
    # → Verwende gespeicherten Wert aus EspoCRM
    stored_kommkz = ESPO_TO_ADVO_KOMMKZ.get(espo_entity['kommunikationstyp'])
    
    # 1. Check ob kommKz in EspoCRM geändert wurde
    if stored_kommkz != espo_entity.get('originalKommKz'):
        # Typ wurde in EspoCRM geändert → Notification
        await notification_manager.notify_manual_action_required(
            entity_type='CKommunikation',
            entity_id=espo_entity['id'],
            action_type='readonly_field_conflict',
            details={
                'readonly_fields': ['kommunikationstyp'],
                'message': 'Kommunikationstyp kann nicht geändert werden',
                'description': (
                    f"Der Kommunikationstyp (kommKz) ist READ-ONLY in Advoware.\n\n"
                    f"**Aktuelle Situation:**\n"
                    f"- Ursprungstyp: {espo_entity.get('originalKommKz')}\n"
                    f"- Neuer Typ: {espo_entity['kommunikationstyp']}\n\n"
                    f"**Workaround:**\n"
                    f"1. Löschen Sie die Kommunikation in EspoCRM\n"
                    f"2. Erstellen Sie sie neu mit dem gewünschten Typ\n"
                    f"3. Die neue Kommunikation wird automatisch nach Advoware synchronisiert"
                ),
                'advoware_id': advoware_id,
                'betnr': betnr
            },
            create_task=True
        )
        return
    
    # 2. Update nur R/W Felder
    advo_data = {
        'kommKz': stored_kommkz,  # WICHTIG: Verwende gespeicherten Wert!
        'tlf': espo_entity['wert'],
        'bemerkung': espo_entity['bemerkung'],
        'online': espo_entity['isOnline']
    }
    
    # 3. PUT zu Advoware
    result = await advoware.api_call(
        f'api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{advoware_id}',
        method='PUT',
        data=advo_data
    )
    
    # 4. Update rowId in EspoCRM
    await espocrm.update_entity('CKommunikation', espo_entity['id'], {
        'advowareRowId': result['rowId'],
        'syncStatus': 'clean',
        'advowareLastSync': datetime.now()
    })
```

**Wichtige Änderungen gegenüber Standard-Pattern**:
- ⚠️ **Kein GET vor PUT**: kommKz ist in GET nicht lesbar (Bug: immer 0)
- ✅ **EspoCRM als Source of Truth**: Verwende gespeicherten kommKz-Wert
- ✅ **originalKommKz Feld**: Speichere ursprünglichen Typ für Änderungserkennung

### 5.3 DELETE - Notification Strategy

```python
async def handle_kommunikation_deletion(espo_entity, betnr):
    """DELETE nicht möglich - Notification für manuelle Löschung"""
    
    advoware_id = espo_entity['advowareId']
    
    await notification_manager.notify_manual_action_required(
        entity_type='CKommunikation',
        entity_id=espo_entity['id'],
        action_type='delete_not_supported',
        details={
            'message': f'DELETE erforderlich für Kommunikation: {espo_entity["name"]}',
            'description': (
                f"Die Advoware API unterstützt keine Löschungen für Kommunikationen.\n\n"
                f"**Bitte manuell in Advoware löschen:**\n"
                f"- Typ: {espo_entity['kommunikationstyp']}\n"
                f"- Wert: {espo_entity['wert']}\n"
                f"- Beteiligter betNr: {betnr}\n"
                f"- Advoware ID: {advoware_id}\n\n"
                f"Die Kommunikation wurde in EspoCRM gelöscht, bleibt aber in Advoware "
                f"bestehen bis zur manuellen Löschung."
            ),
            'betnr': betnr,
            'advoware_id': advoware_id,
            'kommunikationstyp': espo_entity['kommunikationstyp'],
            'wert': espo_entity['wert']
        },
        create_task=True
    )
```

### 5.4 SYNC from Advoware

```python
async def sync_from_advoware(betnr):
    """Sync Kommunikationen Advoware → EspoCRM"""
    
    # 1. Hole alle Kommunikationen vom Beteiligten
    beteiligte = await advoware.api_call(
        f'api/v1/advonet/Beteiligte/{betnr}',
        method='GET'
    )
    
    if isinstance(beteiligte, list):
        beteiligte = beteiligte[0]
    
    advo_kommunikationen = beteiligte.get('kommunikation', [])
    
    # 2. Hole CBeteiligte aus EspoCRM
    espo_beteiligte = await espocrm.list_entities(
        'CBeteiligte',
        filters={'betnr': betnr}
    )
    
    if not espo_beteiligte:
        logger.warning(f"Beteiligter {betnr} nicht in EspoCRM gefunden")
        return
    
    beteiligte_id = espo_beteiligte[0]['id']
    
    # 3. Hole bestehende CKommunikation Entities
    espo_kommunikationen = await espocrm.list_entities(
        'CKommunikation',
        filters={'beteiligteId': beteiligte_id}
    )
    
    # 4. Matche via advowareId
    espo_by_advo_id = {
        k['advowareId']: k 
        for k in espo_kommunikationen 
        if k.get('advowareId')
    }
    
    # 5. Sync jede Advoware-Kommunikation
    for advo_komm in advo_kommunikationen:
        advo_id = advo_komm['id']
        
        if advo_id in espo_by_advo_id:
            # UPDATE bestehende
            espo_komm = espo_by_advo_id[advo_id]
            
            # Check rowId für Änderungen
            if espo_komm.get('advowareRowId') != advo_komm['rowId']:
                # Advoware wurde geändert
                await update_from_advoware(espo_komm, advo_komm)
        else:
            # CREATE neue
            await create_from_advoware(beteiligte_id, advo_komm)
```

---

## 6. Implementierungsplan

### Phase 1: EspoCRM Entity Setup

1. **Entity erstellen**: `CKommunikation`
2. **Felder definieren**:
   - `kommunikationstyp` (enum)
   - `wert` (string)
   - `bemerkung` (text)
   - `isOnline` (bool)
   - `isPrimary` (bool)
   - `beteiligteId` (link zu CBeteiligte)
   - Sync-Felder (advowareId, rowId, syncStatus, etc.)
3. **Relationship**: Many-to-One zu CBeteiligte

### Phase 2: Mapper Implementierung

1. **kommunikation_mapper.py**:
   - `map_ckommunikation_to_advoware_create()` - Alle Felder
   - `map_ckommunikation_to_advoware_update()` - Nur R/W Felder
   - `map_advoware_to_ckommunikation()` - Reverse mapping
   - `detect_readonly_changes()` - kommKz Detection

2. **Enum-Mappings**:
   - `ESPO_TO_ADVO_KOMMKZ`
   - `ADVO_TO_ESPO_KOMMKZ`

### Phase 3: Sync Service

1. **kommunikation_sync.py**:
   - `create_kommunikation()` - POST zu Advoware
   - `update_kommunikation()` - PUT (nur R/W)
   - `handle_kommunikation_deletion()` - Notification
   - `sync_from_advoware()` - Import
   - `_find_kommunikation_by_advoware_id()` - Matching

2. **NotificationManager Integration**:
   - `readonly_field_conflict` - kommKz geändert
   - `delete_not_supported` - Manuelle Löschung

### Phase 4: Webhook Integration

1. **Webhook Endpoints**:
   - `kommunikation_create_api_step.py`
   - `kommunikation_update_api_step.py`
   - `kommunikation_delete_api_step.py`

2. **Event Handler**:
   - `kommunikation_sync_event_step.py`
   - Subscribe: `vmh.kommunikation.{create|update|delete}`

### Phase 5: Testing

1. **Unit Tests**:
   - Mapper-Funktionen
   - Enum-Conversions
   - Readonly-Detection

2. **Integration Tests**:
   - CREATE mit allen kommKz-Typen
   - UPDATE R/W Felder
   - UPDATE kommKz → Notification
   - DELETE → Notification
   - SYNC from Advoware

3. **End-to-End Tests**:
   - Webhook → Sync → Advoware
   - Advoware Änderung → Import
   - Konfliktauflösung

---

## 📊 Zusammenfassung

### ✅ Erfolgreich getestet

- ✅ POST: Alle 4 Felder funktionieren
- ✅ GET: Über Beteiligte-Endpoint verfügbar
- ✅ PUT: 3 von 4 Feldern änderbar (tlf, bemerkung, online)
- ✅ rowId: Ändert sich bei jedem UPDATE (perfekt für Change Detection)

### ❌ Einschränkungen

- ❌ kommKz: READ-ONLY bei PUT (Typ kann nicht geändert werden)
- ❌ DELETE: 403 Forbidden (wie bei Adressen/Bankverbindungen)

### 💡 Empfohlene Sync-Strategie

1. **CREATE**: Automatisch (alle Felder)
2. **UPDATE**: Automatisch (tlf, bemerkung, online) + Notification bei kommKz-Änderung
3. **DELETE**: Notification für manuelle Löschung
4. **SYNC**: Via advowareId + rowId (wie bei Adressen)

### 🔗 Ähnlichkeiten zu Adressen-Sync

- Gleiche Limitationen (kein DELETE)
- Teilweise READ-ONLY Felder bei PUT
- rowId-basierte Change Detection
- advowareId für Matching
- NotificationManager für manuelle Interventionen

**Die Implementierung kann stark an adressen_sync.py angelehnt werden!**

---

## 5. Implementation Details

### 5.1 Implementierte Module

Die Kommunikation-Sync besteht aus 3 Hauptmodulen:

#### **services/kommunikation_mapper.py**
**Zweck**: Datentyp-Mapping und Marker-Verwaltung

**Hauptfunktionen**:
- `calculate_hash(value)`: SHA256[:8] für Matching
- `parse_marker(bemerkung)`: Extrahiert Marker aus bemerkung
- `create_marker(value, kommKz, user_text)`: Erstellt `[ESPOCRM:hash:kommKz]`
- `create_slot_marker(kommKz)`: Erstellt `[ESPOCRM-SLOT:kommKz]`
- `detect_kommkz(value, beteiligte, bemerkung)`: **4-Stufen Typ-Erkennung**
  1. Aus Marker (höchste Priorität)
  2. Aus Top-Level Feldern (telGesch, emailGesch, etc.)
  3. Aus Wert-Pattern (@ = Email, sonst Phone)
  4. Default (MailGesch=4, TelGesch=1)
- `advoware_to_espocrm_email()`: Mapping Advoware → EspoCRM Email
- `advoware_to_espocrm_phone()`: Mapping Advoware → EspoCRM Phone
- `find_matching_advoware()`: Hash-basierte Suche in Advoware
- `find_empty_slot()`: Findet wiederverwendbare leere Slots
- `should_sync_to_espocrm()`: Filtert leere Slots und ungültige Einträge

**Konstanten**:
```python
KOMMKZ_TEL_GESCH = 1
KOMMKZ_FAX_GESCH = 2
KOMMKZ_MOBIL = 3
KOMMKZ_MAIL_GESCH = 4
# ... etc (1-12)

EMAIL_KOMMKZ = [4, 8, 11, 12]  # Mail, MailPrivat, EPost, Bea
PHONE_KOMMKZ = [1, 2, 3, 6, 7, 9, 10]  # Alle Telefon-Typen

KOMMKZ_TO_PHONE_TYPE = {
    1: 'Office',    # TelGesch
    2: 'Fax',       # FaxGesch
    3: 'Mobile',    # Mobil
    6: 'Home',      # TelPrivat
    # ...
}
```

#### **services/advoware_service.py**
**Zweck**: Advoware API-Wrapper für Kommunikation-Operations

```python
class AdvowareService:
    def get_beteiligter(betnr: int) -> Dict:
        """Lädt Beteiligte mit kommunikation[] array"""
    
    def create_kommunikation(betnr: int, data: dict) -> Dict:
        """POST /api/v1/advonet/Beteiligte/{betnr}/Kommunikationen
        
        Required: tlf, kommKz
        Optional: bemerkung, online
        """
    
    def update_kommunikation(betnr: int, komm_id: int, data: dict) -> bool:
        """PUT /api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{komm_id}
        
        Writable: tlf, bemerkung, online
        READ-ONLY: kommKz
        """
    
    def delete_kommunikation(betnr: int, komm_id: int) -> bool:
        """DELETE (aktuell 403 Forbidden)
        
        Nicht verwendbar - nutze Empty Slots stattdessen
        """
```

#### **services/kommunikation_sync_utils.py**
**Zweck**: Bidirektionale Synchronisationslogik

```python
class KommunikationSyncManager:
    def __init__(self, advoware: AdvowareService, espocrm: EspoCrmService):
        pass
    
    # ========== BIDIRECTIONAL ==========
    
    def sync_bidirectional(beteiligte_id: str, betnr: int, direction: str):
        """direction: 'both', 'to_espocrm', 'to_advoware'
        
        Returns: Combined results from both directions
        """
    
    # ========== ADVOWARE → ESPOCRM ==========
    
    def sync_advoware_to_espocrm(beteiligte_id: str, betnr: int):
        """
        Lädt Advoware Kommunikationen → Schreibt zu EspoCRM Arrays
        
        Schritte:
        1. Lade Advoware Beteiligte mit kommunikation[]
        2. Filtere: should_sync_to_espocrm() (keine leeren Slots)
        3. Erkenne Typ: detect_kommkz()
        4. Konvertiere: advoware_to_espocrm_email/phone()
        5. Update EspoCRM: emailAddressData[] und phoneNumberData[]
        
        Returns: {'emails_synced': int, 'phones_synced': int, 'errors': []}
        """
    
    # ========== ESPOCRM → ADVOWARE ==========
    
    def sync_espocrm_to_advoware(beteiligte_id: str, betnr: int):
        """
        Lädt EspoCRM Arrays → Schreibt zu Advoware Kommunikationen
        
        Schritte:
        1. Lade beide Seiten
        2. Baue Hash-Maps: EspoCRM values ↔ Advoware entries
        3. Erkenne Szenarien:
           - Deleted: In Advoware aber nicht in EspoCRM → Empty Slot
           - Changed: Hash match, Wert geändert → UPDATE
           - New: In EspoCRM aber nicht in Advoware → CREATE/REUSE
        
        Returns: {'created': int, 'updated': int, 'deleted': int, 'errors': []}
        """
```

**Change Detection**:
```python
def detect_kommunikation_changes(old_bet: dict, new_bet: dict) -> bool:
    """
    Für Advoware Webhooks
    
    Vergleicht rowId arrays:
    - Anzahl geändert?
    - rowId Set geändert?
    """

def detect_espocrm_kommunikation_changes(old_data: dict, new_data: dict) -> bool:
    """
    Für EspoCRM Webhooks
    
    Vergleicht Arrays:
    - emailAddressData count/values
    - phoneNumberData count/values
    """
```

### 5.2 Integration in Webhook-System

Die Kommunikation-Sync wird in den bestehenden Beteiligte-Webhooks integriert:

**Advoware Webhook** (bei rowId-Änderung):
```python
# In beteiligte_sync_event_handler

from services.advoware_service import AdvowareService
from services.espocrm import EspoCrmService
from services.kommunikation_sync_utils import (
    KommunikationSyncManager,
    detect_kommunikation_changes
)

advo_service = AdvowareService()
espo_service = EspoCrmService()
komm_sync = KommunikationSyncManager(advo_service, espo_service)

# Bei Beteiligte-Update
old_data = previous_beteiligte_data
new_data = current_beteiligte_data

if detect_kommunikation_changes(old_data, new_data):
    logger.info(f"[KOMM] Änderung erkannt für betnr={betnr}")
    result = komm_sync.sync_bidirectional(bet_id, betnr, direction='to_espocrm')
    logger.info(f"[KOMM] Sync-Result: {result}")
```

**EspoCRM Webhook** (bei Array-Änderung):
```python
# In espocrm_webhook_handler

from services.kommunikation_sync_utils import detect_espocrm_kommunikation_changes

# Bei CBeteiligte-Update
old_data = previous_cbeteiligte_data
new_data = current_cbeteiligte_data

if detect_espocrm_kommunikation_changes(old_data, new_data):
    logger.info(f"[KOMM] EspoCRM Änderung erkannt für bet_id={bet_id}")
    result = komm_sync.sync_bidirectional(bet_id, betnr, direction='to_advoware')
    logger.info(f"[KOMM] Sync-Result: {result}")
```

### 5.3 Testing

**Test-Scripts** (bereits im Repo):
- `scripts/test_kommunikation_api.py`: Vollständige API-Tests (POST/PUT/GET/DELETE)
- `scripts/test_kommunikation_kommkz_deep.py`: kommKz-Bug Analyse
- `scripts/test_kommart_values.py`: kommArt-Bug Verifikation
- `scripts/analyze_beteiligte_endpoint.py`: Top-Level Felder Analyse
- `scripts/test_espocrm_kommunikation.py`: EspoCRM Struktur-Tests

**Manuelle Tests**:
1. **Szenario 1** - Löschen in EspoCRM:
   - Lösche Email in EspoCRM
   - Trigger Webhook → Sync
   - Verify: Advoware hat Empty Slot `[ESPOCRM-SLOT:4]`

2. **Szenario 2** - Ändern in EspoCRM:
   - Ändere Email-Wert in EspoCRM
   - Trigger Webhook → Sync
   - Verify: Advoware hat neuen Wert + neuen Hash-Marker

3. **Szenario 3** - Neu in EspoCRM:
   - Füge Email in EspoCRM hinzu
   - Trigger Webhook → Sync
   - Verify: Advoware hat neue Kommunikation ODER reused Slot

4. **Szenario 4** - Neu in Advoware:
   - Erstelle Kommunikation in Advoware
   - Trigger Webhook → Sync
   - Verify: EspoCRM hat neue Email + Advoware hat Marker

---

## 6. Base64-Implementation (Ersetzt Hash-Strategie) ✅

### 6.1 Problem: Hash ist nicht rückrechenbar

**Kritisches Problem der Hash-Strategie**:
```python
# Szenario: User ändert Wert in Advoware
old_value = "old@example.com"
old_hash = calculate_hash(old_value)  # abc12345

# Marker in Advoware bemerkung: [ESPOCRM:abc12345:4]
# EspoCRM hat: old@example.com (mit Hash abc12345)

# USER ÄNDERT in Advoware:
new_value = "new@example.com"
new_hash = calculate_hash(new_value)  # xyz78901

# Sync-Problem:
# - Advoware Marker: [ESPOCRM:abc12345:4] (alter Hash!)
# - EspoCRM sucht: xyz78901 (neuer Hash)
# - Result: ❌ KEIN MATCH! Kann old@example.com nicht finden
```

**Konsequenz**: Hash-basiertes Matching funktioniert nur **einseitig** (EspoCRM → Advoware).

### 6.2 Lösung: Base64-Encoding ✅

**Brillante Idee**: Speichere den **tatsächlichen Wert** (Base64-kodiert) statt Hash!

```python
# Base64-Strategie
old_value = "old@example.com"
encoded = encode_value(old_value)  # b2xkQGV4YW1wbGUuY29t

# Marker in Advoware: [ESPOCRM:b2xkQGV4YW1wbGUuY29t:4]

# USER ÄNDERT in Advoware:
new_value = "new@example.com"

# Sync-Erfolg:
# - Marker enthält: b2xkQGV4YW1wbGUuY29t
# - Dekodiert zu: "old@example.com" ✅
# - Findet Match in EspoCRM!
# - Updated EspoCRM + Marker mit neuem Base64-Wert
```

**Vorteile**:
- ✅ **Bidirektional**: Matching in beide Richtungen
- ✅ **Selbstheilend**: Automatische Marker-Updates bei Wert-Änderungen
- ✅ **Escaping**: Base64 löst `:` und `]` Probleme
- ✅ **Kompakt**: URL-safe Base64 ist kurz genug für bemerkung-Feld

### 6.3 Implementation

**Encoding/Decoding**:
```python
import base64

def encode_value(value: str) -> str:
    """Base64 URL-safe encoding"""
    if not value:
        return ''
    return base64.urlsafe_b64encode(value.encode('utf-8')).decode('ascii').rstrip('=')

def decode_value(encoded: str) -> str:
    """Base64 decoding mit padding"""
    if not encoded:
        return ''
    padding = 4 - (len(encoded) % 4)
    if padding and padding != 4:
        encoded += '=' * padding
    return base64.urlsafe_b64decode(encoded).decode('utf-8')
```

**Marker-Functions**:
```python
def create_marker(value: str, kommkz: int, user_text: str = '') -> str:
    """Erstellt Marker mit Base64-Wert"""
    encoded = encode_value(value)
    suffix = f" {user_text}" if user_text else ""
    return f"[ESPOCRM:{encoded}:{kommkz}]{suffix}"

def parse_marker(bemerkung: str) -> Optional[Dict]:
    """Parse Marker und dekodiere Wert"""
    pattern = r'\[ESPOCRM:([^:]+):(\d+)\](.*)'
    match = re.match(pattern, bemerkung)
    if not match:
        return None
    
    encoded_value = match.group(1)
    synced_value = decode_value(encoded_value)  # Dekodiert!
    
    return {
        'synced_value': synced_value,  # Original-Wert
        'kommKz': int(match.group(2)),
        'is_slot': False,
        'user_text': match.group(3).strip()
    }
```

**Bidirektionales Matching**:
```python
def find_matching_advoware(espo_value: str, advo_kommunikationen: List[Dict]) -> Optional[Dict]:
    """Findet Advoware-Eintrag für EspoCRM-Wert"""
    for komm in advo_kommunikationen:
        bemerkung = komm.get('bemerkung') or ''
        marker = parse_marker(bemerkung)
        
        if marker and marker['synced_value'] == espo_value:
            return komm  # Match via dekodiertem Wert! ✅
    
    return None
```

### 6.4 Test-Ergebnisse ✅

**Alle 7 Tests erfolgreich** (scripts/test_kommunikation_sync_implementation.py):

1. ✅ **Base64-Encoding bidirektional**: 
   - `max@example.com` ↔ `bWF4QGV4YW1wbGUuY29t`
   - Special chars: `test:special]@example.com` ↔ `dGVzdDpzcGVjaWFsXUBleGFtcGxlLmNvbQ`

2. ✅ **Marker-Parsing**: synced_value korrekt dekodiert

3. ✅ **Marker-Erstellung**: Base64-Wert im Marker

4. ✅ **4-Tier Typ-Erkennung**: Marker > Top-Level > Pattern > Default

5. ✅ **Typ-Klassifizierung**: Email vs Phone types

6. ✅ **Integration mit bidirektionalem Matching**:
   ```python
   # Szenario: Wert ändert in Advoware
   old_value = "new@example.com"
   marker = create_marker(old_value, 4)  # [ESPOCRM:bmV3QGV4YW1wbGUuY29t:4]
   
   # User ändert zu:
   new_value = "changed@example.com"
   
   # Sync dekodiert Marker:
   parsed = parse_marker(marker)
   assert parsed['synced_value'] == "new@example.com"  # ✅
   
   # Findet Match in EspoCRM:
   espo_match = find_in_espocrm(parsed['synced_value'])
   # Updates EspoCRM + Marker mit neuem Wert
   ```

7. ✅ **Top-Level Feld Priorität**: telGesch, mobil etc. überschreiben Pattern

### 6.5 Migration von Hash zu Base64

**Backward Compatibility**: `parse_marker()` erkennt alte Hash-Marker automatisch:
```python
if marker and len(encoded_value) == 8 and all(c in '0123456789abcdef' for c in encoded_value):
    # Legacy hash marker → Kann nicht dekodiert werden
    synced_value = encoded_value  # Fallback
else:
    synced_value = decode_value(encoded_value)  # Base64
```

**Automatische Migration**: Beim nächsten Sync werden Hash-Marker automatisch auf Base64 aktualisiert.

### 6.6 Vollständiger Sync-Ablauf mit Base64

**Szenario**:
```
Initial State:
  tlf: "old@example.com"
  bemerkung: "[ESPOCRM:abc12345:4]"
  
User ändert tlf in Advoware:
  tlf: "new@example.com"
  bemerkung: "[ESPOCRM:abc12345:4]"  ← UNVERÄNDERT!
  
Problem:
  calculate_hash("new@example.com") ≠ "abc12345"
  → Matching zu EspoCRM schlägt fehl
```

### 6.2 Lösung: Automatische Hash-Validierung

Die `sync_advoware_to_espocrm()` Funktion validiert ALLE Hashes vor dem Sync:

```python
def sync_advoware_to_espocrm(self, beteiligte_id: str, betnr: int):
    """Mit automatischer Hash-Validierung und Marker-Update"""
    
    for komm in kommunikationen:
        tlf = komm.get('tlf')
        bemerkung = komm.get('bemerkung')
        komm_id = komm.get('id')
        
        marker = parse_marker(bemerkung)
        
        if marker and not marker['is_slot']:
            current_hash = calculate_hash(tlf)
            
            # HASH-MISMATCH → Wert wurde in Advoware geändert
            if marker['hash'] != current_hash:
                logger.info(f"Hash-Mismatch detected: komm_id={komm_id}")
                
                # Update Marker mit neuem Hash (behält User-Text)
                user_text = marker.get('user_text', '')
                new_marker = create_marker(tlf, marker['kommKz'], user_text)
                
                self.advoware.update_kommunikation(betnr, komm_id, {
                    'bemerkung': new_marker
                })
                
                result['markers_updated'] += 1
        
        # ... Rest des Syncs
```

**Vorteile**:
- ✅ Automatische Selbstheilung bei Änderungen in Advoware
- ✅ User-Text wird beibehalten
- ✅ kommKz bleibt erhalten (aus altem Marker)
- ✅ Matching funktioniert beim nächsten Sync wieder

**Result-Struktur** (erweitert):
```python
{
    'emails_synced': 3,
    'phones_synced': 2,
    'markers_updated': 1,  # 🆕 Anzahl korrigierter Marker
    'errors': []
}
```

### 6.3 Integration in Beteiligte-Sync

Der Kommunikation-Sync ist **integraler Bestandteil** des Beteiligte-Syncs:

**Implementierung in `beteiligte_sync_event_step.py`**:

```python
from services.advoware_service import AdvowareService
from services.kommunikation_sync_utils import (
    KommunikationSyncManager,
    detect_kommunikation_changes
)

# In handler()
advo_service = AdvowareService(context)
komm_sync = KommunikationSyncManager(advo_service, espocrm)

# In handle_update()
async def handle_update(entity_id, betnr, espo_entity, espocrm, advoware, ...):
    # 1. Speichere alte Version für Change Detection
    old_advo_entity = advo_entity.copy()
    
    # 2. Sync STAMMDATEN (wie bisher)
    comparison = sync_utils.compare_entities(espo_entity, advo_entity)
    
    if comparison == 'espocrm_newer':
        # Update Advoware Stammdaten
        await advoware.api_call(f'.../Beteiligte/{betnr}', 'PUT', data=merged_data)
        
        # 3. KOMMUNIKATION SYNC (nach Stammdaten)
        advo_entity_refreshed = await advoware.api_call(f'.../Beteiligte/{betnr}', 'GET')
        
        if detect_kommunikation_changes(old_advo_entity, advo_entity_refreshed):
            context.logger.info("📞 Kommunikation-Änderungen erkannt")
            komm_result = komm_sync.sync_bidirectional(entity_id, betnr, direction='to_espocrm')
            context.logger.info(f"✅ Kommunikation synced: {komm_result}")
    
    elif comparison == 'advoware_newer':
        # Update EspoCRM Stammdaten
        await espocrm.update_entity('CBeteiligte', entity_id, espo_data)
        
        # 3. KOMMUNIKATION SYNC
        if detect_kommunikation_changes(old_advo_entity, advo_entity):
            komm_result = komm_sync.sync_bidirectional(entity_id, betnr, direction='to_espocrm')
```

**Reihenfolge ist wichtig**:
1. **Erst** Stammdaten-Sync (name, anrede, etc.)
2. **Dann** Kommunikation-Sync (emails, phones)
3. Change Detection via `rowId` (Stammdaten) + Array-Vergleich (Kommunikation)

**Fehlerbehandlung**:
```python
try:
    komm_result = komm_sync.sync_bidirectional(...)
    context.logger.info(f"✅ Kommunikation synced: {komm_result}")
except Exception as e:
    # Kommunikation-Fehler blockiert NICHT den Stammdaten-Sync
    context.logger.error(f"⚠️ Kommunikation-Sync fehlgeschlagen: {e}")
    # Stammdaten sind bereits gespeichert → syncStatus bleibt 'clean'
```

**Vorteile der Integration**:
- ✅ Atomare Operation: Stammdaten + Kommunikation in einem Durchlauf
- ✅ Keine separaten Webhooks nötig
- ✅ Konsistente Change Detection
- ✅ Fehler-Isolation: Kommunikation-Fehler blockiert nicht Stammdaten-Sync

### 6.4 Vollständiger Sync-Ablauf

**Beispiel: User ändert Email in Advoware**

1. **User-Aktion**: `old@example.com` → `new@example.com` in Advoware
2. **Webhook**: Advoware Beteiligte-Änderung
3. **Stammdaten-Check**: `rowId` geändert → `comparison = 'advoware_newer'`
4. **Kommunikation-Check**: `detect_kommunikation_changes() = True`
5. **Sync Advoware → EspoCRM**:
   - Hash-Validierung: `abc12345 ≠ calculate_hash("new@example.com")`
   - **Marker-Update**: `[ESPOCRM:def67890:4]`
   - **EspoCRM-Update**: `emailAddressData = [{emailAddress: "new@example.com", ...}]`
6. **Result**: `{emails_synced: 1, markers_updated: 1, errors: []}`

**Beispiel: User löscht Email in EspoCRM**

1. **User-Aktion**: Löscht `max@example.com` in EspoCRM
2. **Webhook**: EspoCRM CBeteiligte-Änderung
3. **Kommunikation-Check**: `detect_espocrm_kommunikation_changes() = True`
4. **Sync EspoCRM → Advoware**:
   - Hash-Map: `abc12345` in Advoware, aber nicht in EspoCRM
   - **Empty Slot**: `tlf = '', bemerkung = "[ESPOCRM-SLOT:4]"`
5. **Result**: `{deleted: 1, errors: []}`

---

**Implementation Status: ✅ COMPLETE + INTEGRATED**

**Ende der Analyse** ✅
