Migrate VMH Integration - Phase 3: Core sync handlers & utilities
- Added 5 core service modules:
* services/notification_utils.py: NotificationManager for manual actions (412 lines)
* services/advoware_service.py: Extended Advoware operations wrapper
* services/espocrm_mapper.py: BeteiligteMapper for data transformation (198 lines)
* services/bankverbindungen_mapper.py: BankverbindungenMapper (174 lines)
* services/beteiligte_sync_utils.py: BeteiligteSync with complex logic (663 lines)
- Distributed locking via Redis + syncStatus
- rowId-based change detection (primary) + timestamp fallback
- Retry with exponential backoff (1min, 5min, 15min, 1h, 4h)
- Conflict resolution (EspoCRM wins)
- Soft-delete handling & round-trip validation
- Added 2 event handler steps:
* steps/vmh/beteiligte_sync_event_step.py: Handles vmh.beteiligte.* queue events
- CREATE: New Beteiligte → POST to Advoware
- UPDATE: Bi-directional sync with conflict resolution
- DELETE: Not yet implemented
- SYNC_CHECK: Periodic sync check via cron
* steps/vmh/bankverbindungen_sync_event_step.py: Handles vmh.bankverbindungen.* events
- CREATE: New Bankverbindung → POST to Advoware
- UPDATE/DELETE: Send notification (API limitation - no PUT/DELETE support)
- Added pytz dependency to pyproject.toml (required for timezone handling)
System Status:
- 27 steps registered (14 VMH-specific)
- 8 queue subscriptions active (4 Beteiligte + 4 Bankverbindungen)
- All webhook endpoints operational and emitting queue events
- Sync handlers ready for processing
Note: Kommunikation sync (kommunikation_sync_utils.py) not yet migrated.
Complex bi-directional sync for Telefon/Email/Fax will be added in Phase 4.
Updated MIGRATION_STATUS.md
This commit is contained in:
174
services/bankverbindungen_mapper.py
Normal file
174
services/bankverbindungen_mapper.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""
|
||||
EspoCRM ↔ Advoware Bankverbindungen Mapper
|
||||
|
||||
Transformiert Bankverbindungen zwischen den beiden Systemen
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, List
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BankverbindungenMapper:
|
||||
"""Mapper für CBankverbindungen (EspoCRM) ↔ Bankverbindung (Advoware)"""
|
||||
|
||||
@staticmethod
|
||||
def map_cbankverbindungen_to_advoware(espo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Transformiert EspoCRM CBankverbindungen → Advoware Bankverbindung Format
|
||||
|
||||
Args:
|
||||
espo_entity: CBankverbindungen Entity von EspoCRM
|
||||
|
||||
Returns:
|
||||
Dict für Advoware API (POST/PUT /api/v1/advonet/Beteiligte/{id}/Bankverbindungen)
|
||||
"""
|
||||
logger.debug(f"Mapping EspoCRM → Advoware Bankverbindung: {espo_entity.get('id')}")
|
||||
|
||||
advo_data = {}
|
||||
|
||||
# Bankname
|
||||
bank = espo_entity.get('bank')
|
||||
if bank:
|
||||
advo_data['bank'] = bank
|
||||
|
||||
# Kontonummer (deprecated, aber noch supported)
|
||||
kto_nr = espo_entity.get('kontoNummer')
|
||||
if kto_nr:
|
||||
advo_data['ktoNr'] = kto_nr
|
||||
|
||||
# BLZ (deprecated, aber noch supported)
|
||||
blz = espo_entity.get('blz')
|
||||
if blz:
|
||||
advo_data['blz'] = blz
|
||||
|
||||
# IBAN
|
||||
iban = espo_entity.get('iban')
|
||||
if iban:
|
||||
advo_data['iban'] = iban
|
||||
|
||||
# BIC
|
||||
bic = espo_entity.get('bic')
|
||||
if bic:
|
||||
advo_data['bic'] = bic
|
||||
|
||||
# Kontoinhaber
|
||||
kontoinhaber = espo_entity.get('kontoinhaber')
|
||||
if kontoinhaber:
|
||||
advo_data['kontoinhaber'] = kontoinhaber
|
||||
|
||||
# SEPA Mandat
|
||||
mandatsreferenz = espo_entity.get('mandatsreferenz')
|
||||
if mandatsreferenz:
|
||||
advo_data['mandatsreferenz'] = mandatsreferenz
|
||||
|
||||
mandat_vom = espo_entity.get('mandatVom')
|
||||
if mandat_vom:
|
||||
advo_data['mandatVom'] = mandat_vom
|
||||
|
||||
logger.debug(f"Mapped to Advoware: IBAN={advo_data.get('iban')}, Bank={advo_data.get('bank')}")
|
||||
|
||||
return advo_data
|
||||
|
||||
@staticmethod
|
||||
def map_advoware_to_cbankverbindungen(advo_entity: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Transformiert Advoware Bankverbindung → EspoCRM CBankverbindungen Format
|
||||
|
||||
Args:
|
||||
advo_entity: Bankverbindung von Advoware API
|
||||
|
||||
Returns:
|
||||
Dict für EspoCRM API (POST/PUT /api/v1/CBankverbindungen)
|
||||
"""
|
||||
logger.debug(f"Mapping Advoware → EspoCRM: id={advo_entity.get('id')}")
|
||||
|
||||
espo_data = {
|
||||
'advowareId': advo_entity.get('id'), # Link zu Advoware
|
||||
'advowareRowId': advo_entity.get('rowId'), # Änderungserkennung
|
||||
}
|
||||
|
||||
# Bankname
|
||||
bank = advo_entity.get('bank')
|
||||
if bank:
|
||||
espo_data['bank'] = bank
|
||||
|
||||
# Kontonummer
|
||||
kto_nr = advo_entity.get('ktoNr')
|
||||
if kto_nr:
|
||||
espo_data['kontoNummer'] = kto_nr
|
||||
|
||||
# BLZ
|
||||
blz = advo_entity.get('blz')
|
||||
if blz:
|
||||
espo_data['blz'] = blz
|
||||
|
||||
# IBAN
|
||||
iban = advo_entity.get('iban')
|
||||
if iban:
|
||||
espo_data['iban'] = iban
|
||||
|
||||
# BIC
|
||||
bic = advo_entity.get('bic')
|
||||
if bic:
|
||||
espo_data['bic'] = bic
|
||||
|
||||
# Kontoinhaber
|
||||
kontoinhaber = advo_entity.get('kontoinhaber')
|
||||
if kontoinhaber:
|
||||
espo_data['kontoinhaber'] = kontoinhaber
|
||||
|
||||
# SEPA Mandat
|
||||
mandatsreferenz = advo_entity.get('mandatsreferenz')
|
||||
if mandatsreferenz:
|
||||
espo_data['mandatsreferenz'] = mandatsreferenz
|
||||
|
||||
mandat_vom = advo_entity.get('mandatVom')
|
||||
if mandat_vom:
|
||||
# Konvertiere DateTime zu Date (EspoCRM Format: YYYY-MM-DD)
|
||||
espo_data['mandatVom'] = mandat_vom.split('T')[0] if 'T' in mandat_vom else mandat_vom
|
||||
|
||||
logger.debug(f"Mapped to EspoCRM: IBAN={espo_data.get('iban')}")
|
||||
|
||||
# Entferne None-Werte (EspoCRM Validierung)
|
||||
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 CBankverbindungen
|
||||
advo_entity: Advoware Bankverbindung
|
||||
|
||||
Returns:
|
||||
Liste von Feldnamen die unterschiedlich sind
|
||||
"""
|
||||
mapped_advo = BankverbindungenMapper.map_advoware_to_cbankverbindungen(advo_entity)
|
||||
|
||||
changed = []
|
||||
|
||||
compare_fields = [
|
||||
'bank', 'iban', 'bic', 'kontoNummer', 'blz',
|
||||
'kontoinhaber', 'mandatsreferenz', 'mandatVom',
|
||||
'advowareId', '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
|
||||
Reference in New Issue
Block a user