Files
motia/bitbylaw/docs/ADRESSEN_SYNC_ANALYSE.md
bitbylaw c770f2c8ee feat: Implement address synchronization between EspoCRM and Advoware
- Add AdressenMapper for transforming addresses between EspoCRM and Advoware formats.
- Create AdressenSync class to handle address creation, update, and deletion synchronization.
- Introduce NotificationManager for managing manual intervention notifications in case of sync issues.
- Implement detailed logging for address sync operations and error handling.
- Ensure READ-ONLY field changes are detected and notified for manual resolution.
2026-02-08 14:29:29 +00:00

51 KiB

Adressen-Synchronisation: Analyse EspoCRM ↔ Advoware

Datum: 8. Februar 2026
Status: Analyse abgeschlossen - Implementierung bereit
Zweck: Evaluierung der Sync-Möglichkeiten für Adressen zwischen EspoCRM und Advoware

⚠️ KRITISCHE ERKENNTNISSE (Executive Summary)

Advoware API-Limitierungen (getestet):

  1. DELETE nicht möglich (403 Forbidden)
  2. Nur 4 Felder via PUT änderbar: strasse, plz, ort, anschrift
  3. READ-ONLY Felder: land, postfach, postfachPLZ, standardAnschrift, bemerkung, gueltigVon, gueltigBis, reihenfolgeIndex
  4. bemerkung ist stabil (kann für EspoCRM-ID Matching genutzt werden)
  5. id ist immer 0 (unbrauchbar für Mapping)
  6. ⚠️ reihenfolgeIndex wird nach Löschung neu vergeben (NICHT stabil, NICHT als Identifier nutzbar!)
  7. rowId ändert sich bei jedem PUT (unbrauchbar für Matching)

Konsequenzen:

  • CREATE (POST) funktioniert mit allen Feldern
  • ⚠️ UPDATE (PUT) nur für Haupt-Adressfelder
  • DELETE muss manuell in Advoware erfolgen
  • Soft-Delete via gueltigBis nicht möglich (READ-ONLY)
  • bemerkung ist die EINZIGE stabile Matching-Methode ("EspoCRM-ID: {id}")
  • ⚠️ reihenfolgeIndex für PUT-Endpoint muss VOR jedem Update via GET + Match ermittelt werden

Empfohlene Strategie: → Siehe Abschnitt 12 "FINALE SYNC-STRATEGIE"

Quick Summary:

  • CREATE: Vollautomatisch (alle 11 Felder)
  • UPDATE: Nur R/W Felder (strasse, plz, ort, anschrift)
  • DELETE: Notification + manuelle Löschung
  • ⚠️ READ-ONLY Änderungen: Notification + manuelle Aktion

1. Advoware Adressen-API (Swagger-basiert)

1.1 Verfügbare Endpoints

Advoware bietet drei REST-Endpoints für Adressen-Management:

GET /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen

  • Zweck: Liste aller Adressen eines Beteiligten abrufen
  • Auth: AllLoggedIn
  • Response: Array von Adresse-Objekten
  • Anwendungsfall: Initiale Synchronisation, Polling für Änderungen

POST /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen

  • Zweck: Neue Adresse für einen Beteiligten hinzufügen
  • Auth: AllLoggedIn
  • Request Body: AdresseParameter (JSON)
  • Response: Array von Adresse-Objekten (201 Created)
  • Anwendungsfall: EspoCRM → Advoware Sync beim Erstellen neuer Adressen

PUT /api/v1/advonet/Beteiligte/{beteiligterId}/Adressen/{adresseId}

  • Zweck: Bestehende Adresse ändern
  • Auth: Mitarbeiter (⚠️ höhere Berechtigung!)
  • Request Body: AdresseParameter (JSON)
  • Response: Aktualisierte Adresse (200 OK)
  • Anwendungsfall: EspoCRM → Advoware Sync beim Update

Wichtig: Es gibt keinen DELETE-Endpoint in der Swagger-Definition!


2. Advoware Adressen-Schema

2.1 Adresse (Response-Objekt)

Vollständiges Schema mit allen Feldern:

{
  "id": 123,                       // int32 - Adress-ID
  "beteiligterId": 104860,         // int32 - Referenz zu Beteiligter
  "reihenfolgeIndex": 1,           // int32 - Sortierung
  "rowId": "abc123...",            // string - Änderungserkennung
  
  "strasse": "Musterstraße 123",   // string (max 50) - Straße + Hausnummer
  "plz": "30159",                  // string (max 20) - Postleitzahl
  "ort": "Hannover",               // string (max 39) - Ort
  "land": "DE",                    // string (max 3) - Ländercode (ISO 3166)
  
  "postfach": "10 20 30",          // string (max 20) - Postfach
  "postfachPLZ": "30001",          // string (max 6) - Postfach PLZ
  
  "anschrift": "...",              // string (max 250) - Formatierte Adresse
  "standardAnschrift": true,       // boolean - Ist Hauptadresse?
  
  "bemerkung": "Privat",           // string (max 250) - Kommentar
  "gueltigVon": "2020-01-01T00:00:00",  // datetime - Gültigkeit von
  "gueltigBis": "2025-12-31T00:00:00"   // datetime - Gültigkeit bis
}

2.2 AdresseParameter (Request-Objekt)

Für POST/PUT:

{
  "strasse": "Neue Straße 456",
  "plz": "10115",
  "ort": "Berlin",
  "land": "DE",
  "postfach": null,
  "postfachPLZ": null,
  "anschrift": "Neue Straße 456\n10115 Berlin",
  "standardAnschrift": false,
  "bemerkung": "Geschäftsadresse",
  "gueltigVon": "2026-02-08T00:00:00",
  "gueltigBis": null
}

Felder-Unterschiede:

  • id, beteiligterId, reihenfolgeIndex, rowId werden nicht im Request mitgeschickt (server-generiert)

3. EspoCRM Adressen-Konzept

3.1 EspoCRM Adressmodell ( Verifiziert via API)

EspoCRM hat ein Custom Entity CAdressen mit Beziehung zu CBeteiligte.

Status: Existiert bereits! (5 Adressen in Datenbank)

CAdressen Entity-Struktur

{
  "id": "696f71061b5cbb8ec",
  "name": "Wund",
  "deleted": false,
  
  // 📍 Adressfelder
  "adresseStreet": "Ehlbeek 3",
  "adresseCity": "Burgwedel",
  "adressePostalCode": "30938",
  "adresseCountry": "DE",
  "adresseState": null,
  
  // 🔗 Beziehung zu CBeteiligte
  "beteiligteId": "68e4aef68d2b4fb98",
  "beteiligteName": "ssdasd",
  
  // ⚙️ Business-Felder
  "isActive": true,
  "syncStatus": "clean",
  "description": null,
  "autoapplymietverhaltnisportaluser": true,
  
  // 🔄 Advoware-Sync-Felder
  "adressid": null,                    // Advoware Adressen-ID
  "advowareindexid": null,             // Advoware reihenfolgeIndex?
  "advowareLastSync": null,
  
  // 📊 Metadaten
  "createdAt": "2026-01-20 12:11:50",
  "createdById": "68d65929f18c2afef",
  "createdByName": "Admin",
  "modifiedAt": "2026-01-20 12:11:59",
  "modifiedById": "68d65929f18c2afef",
  "modifiedByName": "Admin",
  
  // Teams & Followers
  "teamsIds": [],
  "teamsNames": {},
  "followersIds": [],
  "followersNames": {},
  "assignedUserId": null,
  "assignedUserName": null,
  "isFollowed": false,
  "streamUpdatedAt": null
}

Vorteile:

  • Beliebig viele Adressen pro Beteiligtem
  • Sync-Felder bereits vorhanden (adressid, advowareLastSync, syncStatus)
  • Standard EspoCRM-Features (Teams, Followers, Assignments)

Beziehung zu CBeteiligte

// CBeteiligte hat Link-Felder:
{
  "id": "6987b30a9bbbfefd0",
  "name": "OptTest86 Musterfrau3",
  "adressensIds": [],              // Array von CAdressen-IDs
  "adressensNames": {}             // Name-Mapping
}

4. Mapping: EspoCRM CAdressen ↔ Advoware Adresse

4.1 Feld-Mapping ( Verifiziert)

EspoCRM Feld Advoware Feld Mapping-Typ Hinweise
adresseStreet strasse ✓ Direkt Straße + Hausnummer
adresseCity ort ✓ Direkt Ort
adressePostalCode plz ✓ Direkt Postleitzahl
adresseCountry land ⚠️ Transform DE ↔ Deutschland
adresseState - Nicht gemappt Bundesland (nur EspoCRM)
beteiligteId beteiligterId ✓ Direkt Link zu Beteiligtem
name - ➜ EspoCRM Adress-Bezeichnung
isActive - ➜ EspoCRM Aktiv-Status
description bemerkung ⚠️ Möglich Beschreibung/Bemerkung
- postfach ← Nur Advoware Postfach
- postfachPLZ ← Nur Advoware Postfach PLZ
- anschrift ← Nur Advoware Formatierte Adresse
- standardAnschrift ← Nur Advoware Ist Hauptadresse?
- gueltigVon ← Nur Advoware Gültigkeit von
- gueltigBis ← Nur Advoware Gültigkeit bis
- reihenfolgeIndex ← Nur Advoware Sortierung

Sync-Felder in EspoCRM (bereits vorhanden!)

EspoCRM Feld Zweck
adressid Advoware Adressen-ID (zum Mapping)
advowareindexid Advoware reihenfolgeIndex
advowareLastSync Timestamp der letzten Synchronisation
syncStatus Status: clean, dirty, error

4.2 Herausforderungen

4.2.1 Anzahl Adressen GELÖST

  • EspoCRM: Beliebig viele Adressen (CAdressen-Entity)
  • Advoware: Beliebig viele Adressen

Status: Kein Problem! Beide Systeme unterstützen mehrere Adressen.

4.2.2 Ländercode-Transformation

  • Advoware: Erwartet 2-3 stelligen Code (z.B. "DE", "AT")
  • EspoCRM: Speichert bereits Kürzel (z.B. "DE")

Status: Bereits kompatibel! Ggf. Fallback für alte Daten mit Vollnamen.

Lösung: Transformation-Tabelle als Fallback:

COUNTRY_CODES = {
    'Deutschland': 'DE',
    'Österreich': 'AT',
    'Schweiz': 'CH',
    'DE': 'DE',  # Already short form
    'AT': 'AT',
    'CH': 'CH',
    # ...
}

4.2.3 Hauptadresse-Flag

  • Advoware: standardAnschrift: true markiert Hauptadresse
  • EspoCRM: Kein dediziertes Feld (nutzt name oder isActive)

Strategie-Optionen:

  1. Erste aktive Adresse = Hauptadresse
  2. Adresse mit name = "Hauptadresse"
  3. Neues Feld isPrimary in EspoCRM hinzufügen

4.2.4 Formatierte Adresse (anschrift)

Advoware hat ein anschrift-Feld für die vollständig formatierte Adresse:

"anschrift": "Musterstraße 123\n30159 Hannover\nDeutschland"

Lösung: EspoCRM generiert diese aus den Einzelfeldern:

def format_anschrift(addr):
    parts = []
    if addr.get('adresseStreet'):
        parts.append(addr['adresseStreet'])
    if addr.get('adressePostalCode') or addr.get('adresseCity'):
        parts.append(f"{addr.get('adressePostalCode', '')} {addr.get('adresseCity', '')}".strip())
    if addr.get('adresseCountry'):
        parts.append(addr['adresseCountry'])
    return '\n'.join(parts)

4.2.5 Postfach-Adressen

  • Advoware: Separate Felder postfach und postfachPLZ
  • EspoCRM: Kein dediziertes Feld

Strategie:

  • Option A: In description speichern
  • Option B: In adresseStreet mit Prefix "Postfach: ..."
  • Option C: Neue Custom-Felder hinzufügen

4.2.6 Zeitliche Gültigkeit

  • Advoware: gueltigVon und gueltigBis (datetime)
  • EspoCRM: Kein dediziertes Feld

Strategie:

  • Option A: Ignorieren (nicht kritisch für Start)
  • Option B: In description speichern
  • Option C: Neue Custom-Felder hinzufügen
  • Option D: isActive = false wenn gueltigBis in Vergangenheit

5. Sync-Strategien

5.1 EMPFOHLEN: CAdressen-Entity (Full Sync)

Voraussetzung: EspoCRM hat bereits CAdressen-Entity mit allen benötigten Feldern

Status: IDEAL - Beide Systeme unterstützen mehrere Adressen

EspoCRM → Advoware

async def sync_addresses_to_advoware(beteiligte_id: str, betnr: int):
    """
    Synct alle CAdressen-Entities zu Advoware
    """
    espo = EspoCRMAPI()
    advo = AdvowareAPI()
    
    # 1. Hole alle Adressen von EspoCRM für diesen Beteiligten
    addresses = await espo.list_entities(
        'CAdressen',
        where=[{
            'type': 'equals',
            'attribute': 'beteiligteId',
            'value': beteiligte_id
        }]
    )
    
    # 2. Hole existierende Adressen von Advoware
    advo_addresses = await advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='GET'
    )
    
    # 3. Sync jede Adresse
    for addr in addresses['list']:
        # Überspringe inaktive Adressen?
        if not addr.get('isActive', True):
            continue
        
        advo_addr_id = addr.get('adressid')  # Gemappte Advoware-ID
        
        advo_data = {
            'strasse': addr.get('adresseStreet'),
            'plz': addr.get('adressePostalCode'),
            'ort': addr.get('adresseCity'),
            'land': addr.get('adresseCountry') or 'DE',
            'anschrift': format_anschrift(addr),
            'bemerkung': addr.get('description'),
            # standardAnschrift: Bestimme via Logic
            'standardAnschrift': is_primary_address(addr, addresses['list'])
        }
        
        if advo_addr_id:
            # Update existierende Adresse
            result = await advo.api_call(
                f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{advo_addr_id}',
                method='PUT',
                json_data=advo_data
            )
        else:
            # Create neue Adresse
            result = await advo.api_call(
                f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
                method='POST',
                json_data=advo_data
            )
            
            # POST gibt Array zurück, nimm erste Adresse
            if isinstance(result, list) and result:
                new_addr = result[0]
            else:
                new_addr = result
            
            # Update EspoCRM mit Advoware-ID
            await espo.update_entity('CAdressen', addr['id'], {
                'adressid': new_addr['id'],
                'advowareindexid': new_addr.get('reihenfolgeIndex'),
                'advowareLastSync': datetime.utcnow().isoformat(),
                'syncStatus': 'clean'
            })

Advoware → EspoCRM

async def sync_addresses_to_espocrm(betnr: int, beteiligte_id: str):
    """
    Synct alle Advoware-Adressen zu EspoCRM CAdressen
    """
    advo = AdvowareAPI()
    espo = EspoCRMAPI()
    
    # 1. Hole Adressen von Advoware
    advo_addresses = await advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='GET'
    )
    
    # 2. Hole existierende CAdressen-Entities
    espo_addresses = await espo.list_entities(
        'CAdressen',
        where=[{
            'type': 'equals',
            'attribute': 'beteiligteId',
            'value': beteiligte_id
        }]
    )
    
    # 3. Mappe via adressid
    espo_by_advo_id = {
        a['adressid']: a 
        for a in espo_addresses['list'] 
        if a.get('adressid')
    }
    
    for advo_addr in advo_addresses:
        espo_data = {
            'name': advo_addr.get('bemerkung') or f"{advo_addr.get('ort')} - {advo_addr.get('strasse')}",
            'beteiligteId': beteiligte_id,
            'adressid': advo_addr['id'],
            'advowareindexid': advo_addr.get('reihenfolgeIndex'),
            'adresseStreet': advo_addr.get('strasse'),
            'adresseCity': advo_addr.get('ort'),
            'adressePostalCode': advo_addr.get('plz'),
            'adresseCountry': advo_addr.get('land'),
            'description': advo_addr.get('bemerkung'),
            'isActive': True,  # Oder basierend auf gueltigBis
            'syncStatus': 'clean',
            'advowareLastSync': datetime.utcnow().isoformat()
        }
        
        existing_espo = espo_by_advo_id.get(advo_addr['id'])
        
        if existing_espo:
            # Update nur wenn rowId sich geändert hat
            if advo_addr.get('rowId') != existing_espo.get('advowareRowId'):
                await espo.update_entity('CAdressen', existing_espo['id'], espo_data)
        else:
            # Create
            await espo.create_entity('CAdressen', espo_data)

Vorteile:

  • Beliebig viele Adressen
  • Vollständiges Mapping aller Felder
  • Sync-Felder bereits vorhanden
  • Keine Datenverluste
  • Entity existiert bereits!

Nachteile:

  • ⚠️ Postfach-Felder fehlen in EspoCRM (optional: Custom Fields)
  • ⚠️ Gültigkeitsdaten fehlen in EspoCRM (optional: Custom Fields)

6. Weitere Sync-Aspekte

6.1 Change Detection

Wie erkennt man Änderungen?

Option A: rowId-Vergleich

Advoware gibt rowId zurück:

def has_changed(advo_addr, espo_addr):
    """Vergleich via rowId"""
    return advo_addr.get('rowId') != espo_addr.get('advowareRowId')

Option B: Timestamp-basiert

def has_changed(advo_addr, espo_addr):
    """Vergleich via lastSyncedAt"""
    last_sync = espo_addr.get('lastSyncedAt')
    if not last_sync:
        return True
    
    # Advoware hat keine Timestamps, nutze rowId
    return advo_addr.get('rowId') != espo_addr.get('advowareRowId')

6.2 Konfliktauflösung

Was passiert bei gleichzeitigen Änderungen?

Master-System definieren

SYNC_MASTER = 'advoware'  # oder 'espocrm'

if SYNC_MASTER == 'advoware':
    # Advoware-Daten überschreiben EspoCRM
    pass
elif SYNC_MASTER == 'espocrm':
    # EspoCRM-Daten überschreiben Advoware
    pass
else:
    # Last-Write-Wins (via Timestamp)
    pass

6.3 DELETE-Handling

Problem: Advoware hat keinen DELETE-Endpoint für Adressen!

Lösungen:

  1. Soft-Delete in Advoware: Setze gueltigBis auf Vergangenheit

    # Statt DELETE:
    await advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{addr_id}',
        method='PUT',
        json_data={'gueltigBis': '1970-01-01T00:00:00'}
    )
    
  2. Ignoriere Deletes: EspoCRM-Löschungen werden nicht zu Advoware propagiert

  3. Marker-Feld: bemerkung: 'GELÖSCHT (EspoCRM)'

6.4 Validierung

Welche Felder sind Pflichtfelder?

Laut Schema sind alle Felder nullable: true, aber Best Practices:

def validate_address(addr_data):
    """Validierung vor Sync"""
    required = ['strasse', 'plz', 'ort']
    
    for field in required:
        if not addr_data.get(field):
            raise ValueError(f"Pflichtfeld fehlt: {field}")
    
    # PLZ-Format (Deutschland)
    if addr_data.get('land') == 'DE':
        plz = addr_data.get('plz', '')
        if not re.match(r'^\d{5}$', plz):
            raise ValueError(f"Ungültige PLZ: {plz}")

7. Empfehlung

7.1 EMPFEHLUNG: CAdressen Full-Sync (Strategie 5.1)

Status: OPTIMAL - EspoCRM hat bereits die perfekte Struktur!

Warum:

  • Entity existiert bereits (CAdressen)
  • Sync-Felder vorhanden (adressid, advowareLastSync, syncStatus)
  • Beliebig viele Adressen möglich
  • Minimale Code-Komplexität
  • Keine Datenverluste
  • Bereits 5 Adressen in Produktion

Implementierungs-Reihenfolge:

  1. Mapper-Klasse AdressenMapper erstellen (analog zu BeteiligteMapper)
  2. EspoCRM → Advoware Sync (CREATE + UPDATE)
  3. Advoware → EspoCRM Sync (CREATE + UPDATE)
  4. Change Detection via adressid + rowId
  5. ⚠️ DELETE-Handling (via Soft-Delete oder ignorieren)

7.2 Optionale Erweiterungen

Falls benötigt:

  • 🔧 Custom-Felder für Postfach hinzufügen (postfach, postfachPLZ)
  • 🔧 Custom-Felder für Gültigkeit hinzufügen (validFrom, validUntil)
  • 🔧 Feld isPrimary für Hauptadresse-Flag

Aktuell:

  • description kann für Bemerkungen genutzt werden
  • isActive kann für abgelaufene Adressen genutzt werden
  • Hauptadresse via Konvention (erste Adresse oder nach Name)

8. Offene Fragen

An EspoCRM-Team (BEANTWORTET):

  1. Gibt es bereits ein Custom Entity für Adressen? → JA: CAdressen
  2. Wie viele Adressen haben Beteiligte typischerweise? → Aktuell 5 in DB
  3. ⚠️ Werden Postfach-Adressen genutzt? → Zu klären
  4. ⚠️ Ist zeitliche Gültigkeit (validFrom/validUntil) relevant? → Zu klären

An Advoware-Team (BEANTWORTET via Tests):

  1. DELETE nicht verfügbar → 403 Forbidden (bestätigt)
  2. Gelöschte Adressen: Nur manuell in Web-Interface möglich
  3. Webhook-Support: Nicht verfügbar
  4. reihenfolgeIndex: System-managed, automatisch ans Ende gereiht

9. FINALE SYNC-STRATEGIE (Nach umfassenden Tests)

BGESCHLOSSEN)

  • Swagger-Dokumentation analysiert
  • EspoCRM CAdressen-Entity via API verifiziert
  • Sync-Strategien evaluiert
  • Empfehlung: Full-Sync mit CAdressen-Entity

Phase 2: Prototyp (Code-Phase) 🚀 NEXT

  • Mapper-Klasse AdressenMapper implementieren
    • map_cadressen_to_advoware(espo_addr) -> dict
    • map_advoware_to_cadressen(advo_addr) -> dict
    • get_changed_fields(espo_addr, advo_addr) -> list
  • Ländercode-Transformation
  • format_anschrift() Helper-Funktion
  • Change Detection via adressid + rowId
  • Unit Tests

Phase 3: Integration

  • Sync-Service AdressenSyncService erstellen
  • Webhook für CAdressen-Changes
  • Adressen-Sync nach Beteiligte-Sync
  • Error Handling & Retry Logic
  • Monitoring & Logging

Phase 4: Testing & Rollout

  • Integration Tests mit echten Daten
  • Performance-Tests (Batch-Sync)
  • Dokumentation
  • Staging Deployment
  • Production Rollout
  • Monitoring & Logging

Erstellt von: GitHub Copilot
Reviewer: [Pending]
Änderungslog:

  • 2026-02-08: Initial Analysis

9.1 Test-Erkenntnisse (08.02.2026)

Durchgeführte Tests:

  1. Alle Felder POST/PUT Verifikation
  2. DELETE-Funktionalität (→ 403 Forbidden)
  3. bemerkung-basiertes Matching mit EspoCRM-IDs
  4. gueltigBis nachträglich setzen (→ READ-ONLY!)
  5. reihenfolgeIndex Verhalten (automatisch, READ-ONLY)
  6. Detaillierte Feld-für-Feld PUT Analyse

Kritische Befunde:

ID-Mapping Problem

  • id ist immer 0 (unbrauchbar)
  • bemerkung ist stabil (READ-ONLY bei PUT) → Perfekt für Matching!
  • reihenfolgeIndex ist stabil (automatisch vergeben, READ-ONLY)
  • rowId ändert sich bei PUT → nicht für Matching nutzen

PUT-Limitierungen

ÄNDERBAR (4 Felder):

  • strasse, plz, ort, anschrift

READ-ONLY (8 Felder):

  • land, postfach, postfachPLZ, standardAnschrift
  • bemerkung (gut für Matching!)
  • gueltigVon, gueltigBis (keine Soft-Delete möglich)
  • reihenfolgeIndex (system-managed)

DELETE Problem

  • DELETE /Adressen/{id} → 403 Forbidden
  • gueltigBis nachträglich setzen → READ-ONLY
  • Soft-Delete NICHT möglich via API!

9.2 Empfohlene Strategie: "Hybrid mit Notifications"

Prinzip: Automatischer Sync wo möglich + manuelle Eingriffe bei API-Limitierungen

Workflow:

  1. CREATE (EspoCRM → Advoware)

    POST /Adressen {
        "bemerkung": f"EspoCRM-ID: {espo_id}",  # Matching-Key
        # ... alle anderen Felder
    }
     Update EspoCRM mit advowareIndexId, advowareRowId
    
  2. UPDATE (EspoCRM → Advoware)

    GET /Adressen  match via bemerkung
    PUT /Adressen/{reihenfolgeIndex} {
        # Nur: strasse, plz, ort, anschrift
    }
     Prüfe READ-ONLY Änderungen
     Notification wenn READ-ONLY geändert
    
  3. DELETE (EspoCRM → Advoware)

    # API unterstützt kein DELETE!
     Notification + Task erstellen
     EspoCRM.isActive = False
     User löscht manuell in Advoware
    
  4. SYNC (Advoware → EspoCRM)

    GET /Adressen  match via bemerkung
     Update/Create in EspoCRM
     Advoware = Master für Existenz
    

9.3 Notification-Management

Zentrale Utility: services/notification_utils.py

Features:

  • Automatische Task-Erstellung in EspoCRM
  • Notification an assigned User
  • Detaillierte Anleitungen für manuelle Eingriffe
  • Verschiedene Action-Types:
    • address_delete_required
    • address_reactivate_required
    • address_field_update_required
    • readonly_field_conflict
    • missing_in_advoware

Beispiel:

from services.notification_utils import NotificationManager

notif_mgr = NotificationManager(espocrm_api, context)

# DELETE erforderlich
await notif_mgr.notify_manual_action_required(
    entity_type='CAdressen',
    entity_id=address_id,
    action_type='address_delete_required',
    details={'betnr': betnr, 'strasse': '...', ...}
)
# → Erstellt Task mit Schritt-für-Schritt Anleitung

9.4 EspoCRM Entity-Anpassungen

Erforderliche Felder in CAdressen:

  • advowareIndexId (int) - für PUT-Endpoint
  • advowareRowId (varchar) - zur Validierung
  • advowareLastSync (datetime) - Sync-Zeitpunkt
  • syncStatus (enum) - synced | partial | manual_action_required | deleted_in_advoware
  • isActive (bool) - Aktiv/Inaktiv Flag
  • manualActionNote (text) - Bei manuellen Eingriffen

9.5 Offene Punkte & Risiken

Risiken:

Risiko Impact Mitigation
User ändert bemerkung Hoch Regex-Parse, auch in Text suchen
DELETE vergessen Mittel Task-System mit Due-Date
READ-ONLY Konflikte Niedrig Notification-System
Parallele Syncs Hoch Locking-Mechanismus

Nächste Schritte:

  1. Notification-System implementiert
  2. EspoCRM CAdressen Entity validieren
  3. Mapper implementieren (services/adressen_mapper.py)
  4. Sync-Service implementieren
  5. Integration Tests

Test-Scripts:


10. Hauptadresse-Logik (standardAnschrift)

10.1 Test-Ergebnisse (08.02.2026)

Durchgeführte Tests:

  • Prüfung ob neue Adressen automatisch zur Hauptadresse werden
  • Explizites Setzen von standardAnschrift beim POST
  • Verhalten bei mehreren Hauptadressen

Kritische Erkenntnisse:

Was funktioniert:

  1. standardAnschrift kann beim POST gesetzt werden

    POST /Adressen {
        "standardAnschrift": true,  # ← Wird akzeptiert!
        ...
    }
    
  2. Feld wird korrekt gespeichert

    • GET zeigt standardAnschrift: true
    • Bleibt persistent

⚠️ Was NICHT funktioniert:

  1. Keine automatische Haupt-Adress-Logik

    • Neue Adressen werden NICHT automatisch zur Hauptadresse
    • standardAnschrift ist standardmäßig false
    • Keine "neueste Adresse = Hauptadresse" Regel
  2. Mehrere Hauptadressen sind möglich!

    • Advoware deaktiviert NICHT alte Hauptadressen automatisch
    • Es können mehrere Adressen standardAnschrift = true haben
    • Keine Validierung auf "nur eine Hauptadresse"
  3. standardAnschrift ist READ-ONLY bei PUT (bereits bekannt)

    • Kann nur beim POST gesetzt werden
    • Kann nicht nachträglich geändert werden

10.2 Konsequenzen für Sync

Probleme:

Szenario 1: User ändert Hauptadresse in EspoCRM
→ PUT zu Advoware → standardAnschrift bleibt unverändert (READ-ONLY)
→ Hauptadresse-Status kann nicht synchronisiert werden!

Szenario 2: Neue Hauptadresse erstellen
→ POST mit standardAnschrift = true
→ Alte Hauptadresse bleibt AUCH true
→ Mehrere Hauptadressen existieren!

Szenario 3: Hauptadresse in Advoware manuell geändert
→ EspoCRM weiß nicht welche die "echte" Hauptadresse ist
→ Wenn mehrere true sind

10.3 Empfohlene Strategie

Option A: EspoCRM als Master (Empfohlen)

# In EspoCRM:
- isPrimary (boolean) - Master-Feld
- advowareStandardAnschrift (boolean) - Read-Only Spiegel

# Sync-Logik:
1. CREATE: Setze standardAnschrift = isPrimary
2. UPDATE: 
   - Wenn isPrimary geändert  Notification (READ-ONLY in Advoware)
3. SYNC from Advoware:
   - Lese alle Adressen
   - Wenn mehrere standardAnschrift = true  Warnungslog
   - Update advowareStandardAnschrift (Info-Feld)
   - isPrimary bleibt EspoCRM-Master

Option B: Advoware als Master

# Sync-Logik:
1. CREATE: Verwende EspoCRM isPrimary initial
2. UPDATE: isPrimary ist READ-ONLY in EspoCRM
3. SYNC from Advoware:
   - Lese standardAnschrift
   - Update isPrimary in EspoCRM
   - Advoware = Master

# Problem: Mehrere Hauptadressen möglich!
 Welche ist die "echte"?
 Nehme erste? Letzte? Niedrigster Index?

Option C: Manuelle Verwaltung

# Bei Hauptadresse-Änderung:
1. User ändert in EspoCRM
2. Notification erstellen
3. User ändert manuell in Advoware Web-Interface
4. Sync aktualisiert EspoCRM

# Vorteil: Keine automatischen Konflikte
# Nachteil: Viel manuelle Arbeit

10.4 Implementierungs-Empfehlung

Hybrid-Ansatz:

class HauptadresseSync:
    """
    Hauptadresse-Sync mit Notification bei Konflikten
    """
    
    async def sync_hauptadresse_status(self, espo_addresses, advo_addresses):
        """Sync Hauptadresse-Status von Advoware"""
        
        # Finde Hauptadressen in Advoware
        advo_haupt = [a for a in advo_addresses if a.get('standardAnschrift')]
        
        if len(advo_haupt) == 0:
            # Keine Hauptadresse in Advoware
            logger.warning(f"Keine Hauptadresse in Advoware für BetNr {betnr}")
            return
        
        if len(advo_haupt) > 1:
            # MEHRERE Hauptadressen!
            logger.error(f"MEHRERE Hauptadressen in Advoware: {len(advo_haupt)}")
            
            # → Notification erstellen
            await notification_manager.notify_manual_action_required(
                entity_type='CBeteiligte',
                entity_id=espo_beteiligte_id,
                action_type='general_manual_action',
                details={
                    'message': f'Mehrere Hauptadressen in Advoware ({len(advo_haupt)})',
                    'description': (
                        f'Beteiligter hat {len(advo_haupt)} Adressen mit '
                        f'standardAnschrift = true. Bitte in Advoware korrigieren, '
                        f'sodass nur EINE Hauptadresse existiert.'
                    ),
                    'priority': 'High'
                }
            )
            
            # Nehme erste als "Haupt" (niedrigster Index)
            advo_haupt = [min(advo_haupt, key=lambda a: a.get('reihenfolgeIndex', 999))]
        
        # Update EspoCRM
        for espo_addr in espo_addresses:
            espo_id = espo_addr['id']
            
            # Finde korrespondierende Advoware-Adresse
            bemerkung_match = f"EspoCRM-ID: {espo_id}"
            advo_addr = next(
                (a for a in advo_addresses 
                 if bemerkung_match in (a.get('bemerkung') or '')),
                None
            )
            
            if advo_addr:
                # Update isPrimary basierend auf Advoware
                should_be_primary = advo_addr in advo_haupt
                
                if espo_addr.get('isPrimary') != should_be_primary:
                    await espocrm_api.update('CAdressen', espo_id, {
                        'isPrimary': should_be_primary,
                        'advowareStandardAnschrift': advo_addr.get('standardAnschrift')
                    })
                    
                    logger.info(
                        f"Updated isPrimary: {espo_addr.get('isPrimary')} "
                        f"→ {should_be_primary} (EspoCRM ID: {espo_id})"
                    )
    
    async def create_with_hauptadresse_logic(self, espo_addr, betnr):
        """Erstelle Adresse mit Hauptadresse-Logik"""
        
        # Wenn diese Adresse Hauptadresse werden soll
        if espo_addr.get('isPrimary'):
            # Prüfe ob es schon eine Hauptadresse gibt
            existing_addresses = await advoware_api.get(f'/Beteiligte/{betnr}/Adressen')
            existing_haupt = [a for a in existing_addresses if a.get('standardAnschrift')]
            
            if len(existing_haupt) > 0:
                # → Notification: Alte Hauptadresse manuell deaktivieren
                await notification_manager.notify_manual_action_required(
                    entity_type='CAdressen',
                    entity_id=espo_addr['id'],
                    action_type='general_manual_action',
                    details={
                        'message': 'Neue Hauptadresse erstellt - alte manuell deaktivieren',
                        'description': (
                            f'Neue Hauptadresse wird erstellt:\n'
                            f'{espo_addr["adresseStreet"]}, {espo_addr["adresseCity"]}\n\n'
                            f'WICHTIG: Alte Hauptadresse(n) in Advoware manuell '
                            f'deaktivieren (standardAnschrift = false setzen):\n' +
                            '\n'.join([f"- {a.get('strasse')}" for a in existing_haupt])
                        ),
                        'priority': 'High'
                    }
                )
        
        # CREATE normal durchführen
        await self.create_address(espo_addr, betnr)

10.5 EspoCRM Entity-Felder

Zusätzlich erforderlich:

{
  "isPrimary": "bool",  // Master-Feld (EspoCRM managed)
  "advowareStandardAnschrift": "bool"  // Info-Feld (Advoware Mirror)
}

10.6 Zusammenfassung

Hauptadresse-Sync ist komplex wegen:

  1. standardAnschrift ist READ-ONLY bei PUT
  2. Mehrere Hauptadressen in Advoware möglich
  3. Keine automatische Deaktivierung alter Hauptadressen

Empfohlener Ansatz:

  • EspoCRM isPrimary als Master
  • Advoware standardAnschrift beim POST setzen
  • Notification bei Hauptadresse-Änderung (manuelle Aktion nötig)
  • Sync from Advoware: Prüfe auf mehrere Hauptadressen → Warnung
  • Wähle niedrigsten Index als "echte" Hauptadresse bei Konflikten

Test-Scripts:


11. reihenfolgeIndex-Instabilität ⚠️

11.1 Test-Ergebnisse (08.02.2026)

Kritische Entdeckung: reihenfolgeIndex wird nach Löschungen automatisch neu vergeben!

Test:

Vorher (24 Adressen):
  Index 1:  Test 6667426
  Index 2:  ...
  Index 23: Hauptadresse Explizit Test
  Index 24: Zweite Hauptadresse Test

Benutzer löscht 23 Adressen in Advoware Web-Interface

Nachher (1 Adresse):
  Index 1: Hauptadresse Explizit Test  ← War vorher Index 23!

11.2 Konsequenzen

reihenfolgeIndex ist NICHT stabil!

# FALSCH - Index ändert sich!
espocrm_record = {
    'advowareIndexId': 23  # ← Nach Löschung nicht mehr gültig!
}

# RICHTIG - Nur bemerkung ist stabil
espocrm_record = {
    'bemerkung': 'EspoCRM-ID: 12345'  # ← Bleibt immer gleich
}

Implikationen:

  1. Kann NICHT in EspoCRM gespeichert werden als Identifier
  2. Kann NICHT für direktes Matching genutzt werden
  3. Muss VOR jedem PUT dynamisch ermittelt werden via:
    # 1. GET alle Adressen
    all_addresses = await advo.get(f'/Beteiligte/{betnr}/Adressen')
    
    # 2. Match via bemerkung
    target = next(
        (a for a in all_addresses 
         if f"EspoCRM-ID: {espo_id}" in (a.get('bemerkung') or '')),
        None
    )
    
    # 3. Nutze aktuellen Index für PUT
    if target:
        await advo.put(
            f'/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}',
            data=updated_data
        )
    

11.3 Update der Sync-Strategie

Korrigierte Workflow:

class AdressenSync:
    async def update_address(self, espo_addr, betnr):
        """
        Update mit dynamischer Index-Ermittlung
        """
        espo_id = espo_addr['id']
        
        # 1. GET alle Adressen (immer!)
        all_addresses = await self.advo.api_call(
            f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
            method='GET'
        )
        
        # 2. Match via bemerkung (EINZIGE stabile Methode)
        bemerkung_match = f"EspoCRM-ID: {espo_id}"
        target = next(
            (a for a in all_addresses 
             if bemerkung_match in (a.get('bemerkung') or '')),
            None
        )
        
        if not target:
            logger.error(f"Adresse nicht gefunden via bemerkung: {bemerkung_match}")
            # → Erstelle neu statt Update?
            return await self.create_address(espo_addr, betnr)
        
        # 3. Nutze AKTUELLEN reihenfolgeIndex für PUT
        current_index = target['reihenfolgeIndex']
        
        # 4. PUT mit aktuellem Index
        updated_data = {
            'strasse': espo_addr['adresseStreet'],
            'plz': espo_addr['adressePostalCode'],
            'ort': espo_addr['adresseCity'],
            'anschrift': self.format_anschrift(espo_addr)
            # Nur 4 Felder erlaubt!
        }
        
        result = await self.advo.api_call(
            f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{current_index}',
            method='PUT',
            json_data=updated_data
        )
        
        logger.info(
            f"Updated Adresse via dynamischen Index {current_index} "
            f"(Match: {bemerkung_match})"
        )
        
        return result

11.4 EspoCRM Felder - KORREKTUR

Alte Annahme (FALSCH):

{
  "advowareIndexId": "int",  // ← NICHT speichern!
  "advowareRowId": "varchar"
}

Neue Empfehlung (RICHTIG):

{
  // KEIN advowareIndexId! (instabil)
  "advowareRowId": "varchar",  // Nur als Info (ändert sich bei PUT)
  "bemerkung": "text"  // Muss "EspoCRM-ID: {id}" enthalten!
}

11.5 Zusammenfassung

Identifier-Stabilität:

Feld Stabil? Nutzung
id Immer 0 Unbrauchbar
rowId Ändert sich bei PUT Nur Change Detection
reihenfolgeIndex Neu vergeben nach Löschung Nur für PUT-Endpoint (dynamisch)
bemerkung Bleibt konstant (READ-ONLY bei PUT) EINZIGER stabiler Identifier!

Empfohlener Workflow:

  1. CREATE: Setze bemerkung = "EspoCRM-ID: {espo_id}"
  2. UPDATE: GET → Match via bemerkung → Nutze aktuellen reihenfolgeIndex für PUT
  3. DELETE: Notification (kein API-DELETE)

Test-Scripts:


11. reihenfolgeIndex-Instabilität ⚠️

11.1 Test-Ergebnisse (08.02.2026)

Kritische Entdeckung: reihenfolgeIndex wird nach Löschungen automatisch neu vergeben!

Test:

Vorher (24 Adressen):
  Index 1:  Test 6667426
  Index 2:  ...
  Index 23: Hauptadresse Explizit Test
  Index 24: Zweite Hauptadresse Test

Benutzer löscht 23 Adressen in Advoware Web-Interface

Nachher (1 Adresse):
  Index 1: Hauptadresse Explizit Test  ← War vorher Index 23!

11.2 Konsequenzen

reihenfolgeIndex ist NICHT stabil!

# FALSCH - Index ändert sich!
espocrm_record = {
    'advowareIndexId': 23  # ← Nach Löschung nicht mehr gültig!
}

# RICHTIG - Nur bemerkung ist stabil
espocrm_record = {
    'bemerkung': 'EspoCRM-ID: 12345'  # ← Bleibt immer gleich
}

Implikationen:

  1. Kann NICHT in EspoCRM gespeichert werden als Identifier
  2. Kann NICHT für direktes Matching genutzt werden
  3. Muss VOR jedem PUT dynamisch ermittelt werden via:
    # 1. GET alle Adressen
    all_addresses = await advo.get(f'/Beteiligte/{betnr}/Adressen')
    
    # 2. Match via bemerkung
    target = next(
        (a for a in all_addresses 
         if f"EspoCRM-ID: {espo_id}" in (a.get('bemerkung') or '')),
        None
    )
    
    # 3. Nutze aktuellen Index für PUT
    if target:
        await advo.put(
            f'/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}',
            data=updated_data
        )
    

11.3 Update der Sync-Strategie

Korrigierter Workflow:

class AdressenSync:
    async def update_address(self, espo_addr, betnr):
        """
        Update mit dynamischer Index-Ermittlung
        """
        espo_id = espo_addr['id']
        
        # 1. GET alle Adressen (immer!)
        all_addresses = await self.advo.api_call(
            f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
            method='GET'
        )
        
        # 2. Match via bemerkung (EINZIGE stabile Methode)
        bemerkung_match = f"EspoCRM-ID: {espo_id}"
        target = next(
            (a for a in all_addresses 
             if bemerkung_match in (a.get('bemerkung') or '')),
            None
        )
        
        if not target:
            logger.error(f"Adresse nicht gefunden via bemerkung: {bemerkung_match}")
            # → Erstelle neu statt Update?
            return await self.create_address(espo_addr, betnr)
        
        # 3. Nutze AKTUELLEN reihenfolgeIndex für PUT
        current_index = target['reihenfolgeIndex']
        
        # 4. PUT mit aktuellem Index
        updated_data = {
            'strasse': espo_addr['adresseStreet'],
            'plz': espo_addr['adressePostalCode'],
            'ort': espo_addr['adresseCity'],
            'anschrift': self.format_anschrift(espo_addr)
            # Nur 4 Felder erlaubt!
        }
        
        result = await self.advo.api_call(
            f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{current_index}',
            method='PUT',
            json_data=updated_data
        )
        
        logger.info(
            f"Updated Adresse via dynamischen Index {current_index} "
            f"(Match: {bemerkung_match})"
        )
        
        return result

11.4 EspoCRM Felder - KORREKTUR

Alte Annahme (FALSCH):

{
  "advowareIndexId": "int",  // ← NICHT speichern!
  "advowareRowId": "varchar"
}

Neue Empfehlung (RICHTIG):

{
  // KEIN advowareIndexId! (instabil)
  "advowareRowId": "varchar",  // Nur als Info (ändert sich bei PUT)
  "bemerkung": "text"  // Muss "EspoCRM-ID: {id}" enthalten!
}

11.5 Zusammenfassung

Identifier-Stabilität:

Feld Stabil? Nutzung
id Immer 0 Unbrauchbar
rowId Ändert sich bei PUT Nur Change Detection
reihenfolgeIndex Neu vergeben nach Löschung Nur für PUT-Endpoint (dynamisch)
bemerkung Bleibt konstant (READ-ONLY bei PUT) EINZIGER stabiler Identifier!

Empfohlener Workflow:

  1. CREATE: Setze bemerkung = "EspoCRM-ID: {espo_id}"
  2. UPDATE: GET → Match via bemerkung → Nutze aktuellen reihenfolgeIndex für PUT
  3. DELETE: Notification (kein API-DELETE)

Test-Scripts:


12. FINALE SYNC-STRATEGIE

12.1 Entscheidung (08.02.2026)

Nach umfangreichen Tests wurde folgende finale Strategie festgelegt:

✅ CREATE  - Vollständig unterstützt (alle 11 Felder)
✅ UPDATE  - Nur für R/W Felder (strasse, plz, ort, anschrift)
❌ DELETE  - NICHT automatisch (nur via Notification)
⚠️ READ-ONLY Änderungen - Nur via Notification + manuelle Aktion

12.2 Implementierungs-Regeln

12.2.1 CREATE (EspoCRM → Advoware)

Vollständig automatisiert:

async def create_address(self, espo_addr, betnr):
    """
    Erstelle neue Adresse in Advoware
    Alle Felder werden synchronisiert
    """
    advo_data = {
        # R/W Felder (via PUT änderbar)
        'strasse': espo_addr.get('adresseStreet'),
        'plz': espo_addr.get('adressePostalCode'),
        'ort': espo_addr.get('adresseCity'),
        'anschrift': self.format_anschrift(espo_addr),
        
        # READ-ONLY Felder (nur bei CREATE!)
        'land': espo_addr.get('adresseCountry') or 'DE',
        'postfach': espo_addr.get('postfach'),
        'postfachPLZ': espo_addr.get('postfachPLZ'),
        'standardAnschrift': espo_addr.get('isPrimary', False),
        'bemerkung': f"EspoCRM-ID: {espo_addr['id']}",  # Matching!
        'gueltigVon': espo_addr.get('validFrom'),
        'gueltigBis': espo_addr.get('validUntil')
    }
    
    result = await self.advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='POST',
        json_data=advo_data
    )
    
    logger.info(f"Created address in Advoware for EspoCRM ID {espo_addr['id']}")
    return result

12.2.2 UPDATE (EspoCRM → Advoware)

Nur R/W Felder, REST über Notification:

async def update_address(self, espo_addr, betnr):
    """
    Update nur R/W Felder
    Alle anderen Änderungen → Notification
    """
    espo_id = espo_addr['id']
    
    # 1. Finde Adresse via bemerkung
    all_addresses = await self.advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='GET'
    )
    
    bemerkung_match = f"EspoCRM-ID: {espo_id}"
    target = next(
        (a for a in all_addresses 
         if bemerkung_match in (a.get('bemerkung') or '')),
        None
    )
    
    if not target:
        logger.error(f"Address not found: {bemerkung_match}")
        return await self.create_address(espo_addr, betnr)
    
    # 2. Update NUR R/W Felder
    rw_data = {
        'strasse': espo_addr.get('adresseStreet'),
        'plz': espo_addr.get('adressePostalCode'),
        'ort': espo_addr.get('adresseCity'),
        'anschrift': self.format_anschrift(espo_addr)
    }
    
    result = await self.advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen/{target["reihenfolgeIndex"]}',
        method='PUT',
        json_data=rw_data
    )
    
    # 3. Prüfe READ-ONLY Felder auf Änderungen
    readonly_changes = self.detect_readonly_changes(espo_addr, target)
    
    if readonly_changes:
        # → Notification für manuelle Änderung
        await self.notify_readonly_changes(espo_addr, readonly_changes)
    
    logger.info(f"Updated address (R/W fields only) for EspoCRM ID {espo_id}")
    return result


def detect_readonly_changes(self, espo_addr, advo_addr):
    """
    Erkenne Änderungen an READ-ONLY Feldern
    """
    changes = []
    
    # Prüfe alle READ-ONLY Felder
    readonly_mappings = {
        'adresseCountry': ('land', 'Land'),
        'postfach': ('postfach', 'Postfach'),
        'postfachPLZ': ('postfachPLZ', 'Postfach PLZ'),
        'isPrimary': ('standardAnschrift', 'Hauptadresse'),
        'validFrom': ('gueltigVon', 'Gültig von'),
        'validUntil': ('gueltigBis', 'Gültig bis')
    }
    
    for espo_field, (advo_field, label) in readonly_mappings.items():
        espo_value = espo_addr.get(espo_field)
        advo_value = advo_addr.get(advo_field)
        
        # Normalisiere Werte für Vergleich
        if espo_field == 'isPrimary':
            espo_value = bool(espo_value)
            advo_value = bool(advo_value)
        
        if espo_value != advo_value:
            changes.append({
                'field': label,
                'espoField': espo_field,
                'advoField': advo_field,
                'espoCRM_value': espo_value,
                'advoware_value': advo_value
            })
    
    return changes


async def notify_readonly_changes(self, espo_addr, changes):
    """
    Erstelle Notification für READ-ONLY Feld-Änderungen
    """
    change_details = '\n'.join([
        f"- {c['field']}: EspoCRM='{c['espoCRM_value']}' → "
        f"Advoware='{c['advoware_value']}'"
        for c in changes
    ])
    
    await self.notification_manager.notify_manual_action_required(
        entity_type='CAdressen',
        entity_id=espo_addr['id'],
        action_type='readonly_field_conflict',
        details={
            'message': f'{len(changes)} READ-ONLY Feld(er) geändert',
            'description': (
                f'Folgende Felder wurden in EspoCRM geändert, sind aber '
                f'READ-ONLY in Advoware und können nicht automatisch '
                f'synchronisiert werden:\n\n{change_details}\n\n'
                f'Bitte manuell in Advoware anpassen.'
            ),
            'changes': changes,
            'address': f"{espo_addr.get('adresseStreet')}, "
                      f"{espo_addr.get('adresseCity')}",
            'priority': 'High'
        }
    )

12.2.3 DELETE (EspoCRM → Advoware)

NUR via Notification:

async def handle_address_deletion(self, espo_addr, betnr):
    """
    Adresse wurde in EspoCRM gelöscht
    → Nur Notification (kein API-DELETE verfügbar)
    """
    
    # 1. Finde Adresse in Advoware
    all_addresses = await self.advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='GET'
    )
    
    bemerkung_match = f"EspoCRM-ID: {espo_addr['id']}"
    target = next(
        (a for a in all_addresses 
         if bemerkung_match in (a.get('bemerkung') or '')),
        None
    )
    
    if not target:
        logger.info(f"Address already deleted: {espo_addr['id']}")
        return
    
    # 2. Notification erstellen
    await self.notification_manager.notify_manual_action_required(
        entity_type='CAdressen',
        entity_id=espo_addr['id'],
        action_type='address_delete_required',
        details={
            'message': 'Adresse in Advoware löschen',
            'description': (
                f'Adresse wurde in EspoCRM gelöscht:\n'
                f'{target.get("strasse")}\n'
                f'{target.get("plz")} {target.get("ort")}\n\n'
                f'Bitte manuell in Advoware löschen:\n'
                f'1. Öffne Beteiligten {betnr} in Advoware\n'
                f'2. Gehe zu Adressen-Tab\n'
                f'3. Lösche Adresse (Index {target.get("reihenfolgeIndex")})\n'
                f'4. Speichern'
            ),
            'advowareIndex': target.get('reihenfolgeIndex'),
            'betnr': betnr,
            'address': f"{target.get('strasse')}, {target.get('ort')}",
            'priority': 'Medium'
        }
    )
    
    logger.info(f"Created delete notification for address {espo_addr['id']}")

12.2.4 SYNC (Advoware → EspoCRM)

Volle Synchronisation (read-only):

async def sync_from_advoware(self, betnr, espo_beteiligte_id):
    """
    Synct Adressen von Advoware zu EspoCRM
    Alle Felder werden übernommen (Advoware = Master)
    """
    
    # 1. Hole alle Adressen von Advoware
    advo_addresses = await self.advo.api_call(
        f'/api/v1/advonet/Beteiligte/{betnr}/Adressen',
        method='GET'
    )
    
    # 2. Hole existierende EspoCRM Adressen
    espo_addresses = await self.espo.list_entities(
        'CAdressen',
        where=[{
            'type': 'equals',
            'attribute': 'beteiligteId',
            'value': espo_beteiligte_id
        }]
    )
    
    for advo_addr in advo_addresses:
        # Match via bemerkung
        bemerkung = advo_addr.get('bemerkung', '')
        
        if 'EspoCRM-ID:' in bemerkung:
            # Update existierende
            espo_id = bemerkung.split('EspoCRM-ID:')[1].strip().split()[0]
            await self.update_espo_address(espo_id, advo_addr)
        else:
            # Neue Adresse aus Advoware
            await self.create_espo_address(advo_addr, espo_beteiligte_id)
    
    logger.info(f"Synced {len(advo_addresses)} addresses from Advoware")

12.3 Zusammenfassung

Automatische Synchronisation:

  • CREATE: Alle Felder (11 Felder)
  • UPDATE: Nur R/W Felder (4 Felder: strasse, plz, ort, anschrift)
  • SYNC from Advoware: Alle Felder (read-only)

Manuelle Aktionen (via Notification):

  • ⚠️ DELETE: Notification + manuelle Löschung in Advoware
  • ⚠️ READ-ONLY Änderungen: Notification bei:
    • land geändert
    • postfach/postfachPLZ geändert
    • standardAnschrift (Hauptadresse) geändert
    • gueltigVon/gueltigBis geändert
    • bemerkung geändert (würde Matching zerstören!)

Rationale:

  1. Keine Datenkorruption: Nur sichere Operationen automatisch
  2. Keine API-Limitierungen umgehen: Respektiere READ-ONLY Status
  3. Volle Transparenz: User sieht alle manuellen Aktionen
  4. Matching bleibt stabil: bemerkung niemals ändern!

Test-Scripts: