- Added `config.py` for centralized configuration management including Sync, API, Advoware, EspoCRM, Redis, Logging, Calendar Sync, and Feature Flags. - Created `exceptions.py` with a hierarchy of custom exceptions for integration errors, API errors, sync errors, and Redis errors. - Developed `logging_utils.py` for a unified logging wrapper supporting structured logging and performance tracking. - Defined Pydantic models in `models.py` for data validation of Advoware and EspoCRM entities, including sync operation models. - Introduced `redis_client.py` for a centralized Redis client factory with connection pooling, automatic reconnection, and health checks.
215 lines
7.8 KiB
Python
215 lines
7.8 KiB
Python
"""
|
|
EspoCRM ↔ Advoware Entity Mapper
|
|
|
|
Transformiert Beteiligte zwischen den beiden Systemen basierend auf ENTITY_MAPPING_CBeteiligte_Advoware.md
|
|
"""
|
|
|
|
from typing import Dict, Any, Optional, List
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
from services.models import (
|
|
AdvowareBeteiligteCreate,
|
|
AdvowareBeteiligteUpdate,
|
|
EspoCRMBeteiligteCreate,
|
|
validate_beteiligte_advoware,
|
|
validate_beteiligte_espocrm
|
|
)
|
|
from services.exceptions import ValidationError
|
|
from services.config import FEATURE_FLAGS
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BeteiligteMapper:
|
|
"""Mapper für CBeteiligte (EspoCRM) ↔ Beteiligte (Advoware)"""
|
|
|
|
@staticmethod
|
|
def map_cbeteiligte_to_advoware(espo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Transformiert EspoCRM CBeteiligte → Advoware Beteiligte Format (STAMMDATEN)
|
|
|
|
WICHTIG: Kontaktdaten (Telefon, Email, Fax, Bankverbindungen) werden über
|
|
separate Advoware-Endpoints gesynct und sind NICHT Teil dieser Mapping-Funktion.
|
|
|
|
Args:
|
|
espo_entity: CBeteiligte Entity von EspoCRM
|
|
|
|
Returns:
|
|
Dict mit Stammdaten für Advoware API (POST/PUT /api/v1/advonet/Beteiligte)
|
|
|
|
Raises:
|
|
ValidationError: Bei Validierungsfehlern (wenn strict_validation aktiviert)
|
|
"""
|
|
logger.debug(f"Mapping EspoCRM → Advoware STAMMDATEN: {espo_entity.get('id')}")
|
|
|
|
# Bestimme ob Person oder Firma (über firmenname-Feld)
|
|
firmenname = espo_entity.get('firmenname')
|
|
is_firma = bool(firmenname and firmenname.strip())
|
|
|
|
# Basis-Struktur (nur die funktionierenden Felder!)
|
|
advo_data = {
|
|
'rechtsform': espo_entity.get('rechtsform', ''),
|
|
}
|
|
|
|
# NAME: Person vs. Firma
|
|
if is_firma:
|
|
# Firma: Lese von firmenname-Feld
|
|
advo_data['name'] = firmenname
|
|
advo_data['vorname'] = None
|
|
else:
|
|
# Natürliche Person: Lese von lastName/firstName
|
|
advo_data['name'] = espo_entity.get('lastName', '')
|
|
advo_data['vorname'] = espo_entity.get('firstName', '')
|
|
|
|
# ANREDE & TITEL (funktionierende Felder)
|
|
salutation = espo_entity.get('salutationName')
|
|
if salutation:
|
|
advo_data['anrede'] = salutation
|
|
|
|
titel = espo_entity.get('titel')
|
|
if titel:
|
|
advo_data['titel'] = titel
|
|
|
|
# BRIEFANREDE (bAnrede)
|
|
brief_anrede = espo_entity.get('briefAnrede')
|
|
if brief_anrede:
|
|
advo_data['bAnrede'] = brief_anrede
|
|
|
|
# ZUSATZ
|
|
zusatz = espo_entity.get('zusatz')
|
|
if zusatz:
|
|
advo_data['zusatz'] = zusatz
|
|
|
|
# GEBURTSDATUM
|
|
date_of_birth = espo_entity.get('dateOfBirth')
|
|
if date_of_birth:
|
|
advo_data['geburtsdatum'] = date_of_birth
|
|
|
|
# HINWEIS: handelsRegisterNummer und registergericht funktionieren NICHT!
|
|
# Advoware ignoriert diese Felder im PUT (trotz Swagger Schema)
|
|
|
|
logger.debug(f"Mapped to Advoware STAMMDATEN: name={advo_data.get('name')}, vorname={advo_data.get('vorname')}, rechtsform={advo_data.get('rechtsform')}")
|
|
|
|
# Optional: Validiere mit Pydantic wenn aktiviert
|
|
if FEATURE_FLAGS.strict_validation:
|
|
try:
|
|
validate_beteiligte_advoware(advo_data)
|
|
except ValidationError as e:
|
|
logger.warning(f"Validation warning: {e}")
|
|
# Continue anyway - validation ist optional
|
|
|
|
return advo_data
|
|
|
|
@staticmethod
|
|
def map_advoware_to_cbeteiligte(advo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""
|
|
Transformiert Advoware Beteiligte → EspoCRM CBeteiligte Format
|
|
|
|
Args:
|
|
advo_entity: Beteiligter von Advoware API
|
|
|
|
Returns:
|
|
Dict für EspoCRM API (POST/PUT /api/v1/CBeteiligte)
|
|
"""
|
|
logger.debug(f"Mapping Advoware → EspoCRM: betNr={advo_entity.get('betNr')}")
|
|
|
|
# Bestimme ob Person oder Firma
|
|
vorname = advo_entity.get('vorname')
|
|
is_person = bool(vorname)
|
|
|
|
# Basis-Struktur
|
|
espo_data = {
|
|
'rechtsform': advo_entity.get('rechtsform', ''),
|
|
'betnr': advo_entity.get('betNr'), # Link zu Advoware
|
|
'advowareRowId': advo_entity.get('rowId'), # Änderungserkennung
|
|
}
|
|
|
|
# NAME: Person vs. Firma (EspoCRM blendet lastName/firstName aus bei Firmen)
|
|
if is_person:
|
|
# Natürliche Person → lastName/firstName verwenden
|
|
espo_data['firstName'] = vorname
|
|
espo_data['lastName'] = advo_entity.get('name', '')
|
|
espo_data['name'] = f"{vorname} {advo_entity.get('name', '')}".strip()
|
|
espo_data['firmenname'] = None # Firma-Feld leer lassen
|
|
else:
|
|
# Firma → firmenname verwenden (EspoCRM zeigt dann nur dieses Feld)
|
|
firma_name = advo_entity.get('name', '')
|
|
espo_data['firmenname'] = firma_name
|
|
espo_data['name'] = firma_name
|
|
# lastName/firstName nicht setzen (EspoCRM blendet sie aus bei Firmen)
|
|
espo_data['firstName'] = None
|
|
espo_data['lastName'] = None
|
|
|
|
# ANREDE & TITEL
|
|
anrede = advo_entity.get('anrede')
|
|
if anrede:
|
|
espo_data['salutationName'] = anrede
|
|
|
|
titel = advo_entity.get('titel')
|
|
if titel:
|
|
espo_data['titel'] = titel
|
|
|
|
# BRIEFANREDE
|
|
b_anrede = advo_entity.get('bAnrede')
|
|
if b_anrede:
|
|
espo_data['briefAnrede'] = b_anrede
|
|
|
|
# ZUSATZ
|
|
zusatz = advo_entity.get('zusatz')
|
|
if zusatz:
|
|
espo_data['zusatz'] = zusatz
|
|
|
|
# GEBURTSDATUM (nur Datum-Teil ohne Zeit)
|
|
geburtsdatum = advo_entity.get('geburtsdatum')
|
|
if geburtsdatum:
|
|
# Advoware gibt '2001-01-05T00:00:00', EspoCRM will nur '2001-01-05'
|
|
espo_data['dateOfBirth'] = geburtsdatum.split('T')[0] if 'T' in geburtsdatum else geburtsdatum
|
|
|
|
logger.debug(f"Mapped to EspoCRM STAMMDATEN: name={espo_data.get('name')}")
|
|
|
|
# WICHTIG: Entferne None-Werte (EspoCRM mag keine expliziten None bei required fields)
|
|
espo_data = {k: v for k, v in espo_data.items() if v is not None}
|
|
|
|
return espo_data
|
|
|
|
@staticmethod
|
|
def get_changed_fields(espo_entity: Dict[str, Any], advo_entity: Dict[str, Any]) -> List[str]:
|
|
"""
|
|
Vergleicht zwei Entities und gibt Liste der geänderten Felder zurück
|
|
|
|
Args:
|
|
espo_entity: EspoCRM CBeteiligte
|
|
advo_entity: Advoware Beteiligte
|
|
|
|
Returns:
|
|
Liste von Feldnamen die unterschiedlich sind
|
|
"""
|
|
# Mappe Advoware zu EspoCRM Format für Vergleich
|
|
mapped_advo = BeteiligteMapper.map_advoware_to_cbeteiligte(advo_entity)
|
|
|
|
changed = []
|
|
|
|
# Vergleiche wichtige Felder
|
|
compare_fields = [
|
|
'name', 'firstName', 'lastName', 'firmenname',
|
|
'emailAddress', 'phoneNumber',
|
|
'dateOfBirth', 'rechtsform',
|
|
'handelsregisterNummer', 'handelsregisterArt', 'registergericht',
|
|
'betnr', 'advowareRowId'
|
|
]
|
|
|
|
for field in compare_fields:
|
|
espo_val = espo_entity.get(field)
|
|
advo_val = mapped_advo.get(field)
|
|
|
|
# Normalisiere None und leere Strings
|
|
espo_val = espo_val if espo_val else None
|
|
advo_val = advo_val if advo_val else None
|
|
|
|
if espo_val != advo_val:
|
|
changed.append(field)
|
|
logger.debug(f"Field '{field}' changed: EspoCRM='{espo_val}' vs Advoware='{advo_val}'")
|
|
|
|
return changed
|