- 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
121 lines
3.9 KiB
Python
121 lines
3.9 KiB
Python
"""
|
|
Advoware Service Wrapper
|
|
Erweitert AdvowareAPI mit höheren Operations
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, Any, Optional
|
|
from services.advoware import AdvowareAPI
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AdvowareService:
|
|
"""
|
|
Service-Layer für Advoware Operations
|
|
Verwendet AdvowareAPI für API-Calls
|
|
"""
|
|
|
|
def __init__(self, context=None):
|
|
self.api = AdvowareAPI(context)
|
|
self.context = context
|
|
|
|
# ========== BETEILIGTE ==========
|
|
|
|
async def get_beteiligter(self, betnr: int) -> Optional[Dict]:
|
|
"""
|
|
Lädt Beteiligten mit allen Daten
|
|
|
|
Returns:
|
|
Beteiligte-Objekt
|
|
"""
|
|
try:
|
|
endpoint = f"api/v1/advonet/Beteiligte/{betnr}"
|
|
result = await self.api.api_call(endpoint, method='GET')
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"[ADVO] Fehler beim Laden von Beteiligte {betnr}: {e}", exc_info=True)
|
|
return None
|
|
|
|
# ========== KOMMUNIKATION ==========
|
|
|
|
async def create_kommunikation(self, betnr: int, data: Dict[str, Any]) -> Optional[Dict]:
|
|
"""
|
|
Erstellt neue Kommunikation
|
|
|
|
Args:
|
|
betnr: Beteiligten-Nummer
|
|
data: {
|
|
'tlf': str, # Required
|
|
'bemerkung': str, # Optional
|
|
'kommKz': int, # Required (1-12)
|
|
'online': bool # Optional
|
|
}
|
|
|
|
Returns:
|
|
Neue Kommunikation mit 'id'
|
|
"""
|
|
try:
|
|
endpoint = f"api/v1/advonet/Beteiligte/{betnr}/Kommunikationen"
|
|
result = await self.api.api_call(endpoint, method='POST', json_data=data)
|
|
|
|
if result:
|
|
logger.info(f"[ADVO] ✅ Created Kommunikation: betnr={betnr}, kommKz={data.get('kommKz')}")
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"[ADVO] Fehler beim Erstellen von Kommunikation: {e}", exc_info=True)
|
|
return None
|
|
|
|
async def update_kommunikation(self, betnr: int, komm_id: int, data: Dict[str, Any]) -> bool:
|
|
"""
|
|
Aktualisiert bestehende Kommunikation
|
|
|
|
Args:
|
|
betnr: Beteiligten-Nummer
|
|
komm_id: Kommunikation-ID
|
|
data: {
|
|
'tlf': str, # Optional
|
|
'bemerkung': str, # Optional
|
|
'online': bool # Optional
|
|
}
|
|
|
|
NOTE: kommKz ist READ-ONLY und kann nicht geändert werden
|
|
|
|
Returns:
|
|
True wenn erfolgreich
|
|
"""
|
|
try:
|
|
endpoint = f"api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{komm_id}"
|
|
await self.api.api_call(endpoint, method='PUT', json_data=data)
|
|
|
|
logger.info(f"[ADVO] ✅ Updated Kommunikation: betnr={betnr}, komm_id={komm_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"[ADVO] Fehler beim Update von Kommunikation: {e}", exc_info=True)
|
|
return False
|
|
|
|
async def delete_kommunikation(self, betnr: int, komm_id: int) -> bool:
|
|
"""
|
|
Löscht Kommunikation (aktuell 403 Forbidden)
|
|
|
|
NOTE: DELETE ist in Advoware API deaktiviert
|
|
Verwende stattdessen: Leere Slots mit empty_slot_marker
|
|
|
|
Returns:
|
|
True wenn erfolgreich
|
|
"""
|
|
try:
|
|
endpoint = f"api/v1/advonet/Beteiligte/{betnr}/Kommunikationen/{komm_id}"
|
|
await self.api.api_call(endpoint, method='DELETE')
|
|
|
|
logger.info(f"[ADVO] ✅ Deleted Kommunikation: betnr={betnr}, komm_id={komm_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
# Expected: 403 Forbidden
|
|
logger.warning(f"[ADVO] DELETE not allowed (expected): {e}")
|
|
return False
|