684 lines
32 KiB
Python
684 lines
32 KiB
Python
from services.advoware import AdvowareAPI
|
|
from config import Config
|
|
from googleapiclient.discovery import build
|
|
from google.oauth2 import service_account
|
|
from googleapiclient.errors import HttpError
|
|
import json
|
|
import datetime
|
|
import redis
|
|
import os
|
|
import hashlib
|
|
import asyncio
|
|
import random
|
|
|
|
# Salt für Token-Generierung laden
|
|
CALENDAR_SYNC_SALT = os.getenv('CALENDAR_SYNC_SALT')
|
|
if not CALENDAR_SYNC_SALT:
|
|
raise ValueError("CALENDAR_SYNC_SALT environment variable not set")
|
|
|
|
def generate_change_token(frnr):
|
|
"""Generiert einen Change-Token mit Salt für eine frNr"""
|
|
return hashlib.md5((str(frnr) + CALENDAR_SYNC_SALT).encode()).hexdigest()
|
|
|
|
config = {
|
|
'type': 'event',
|
|
'name': 'Calendar Sync Event Handler',
|
|
'description': 'Führt den Advoware-Google Calendar Sync aus bei Events',
|
|
'subscribes': ['calendar.sync.triggered'],
|
|
'emits': []
|
|
}
|
|
|
|
async def google_api_call_with_backoff(call_func, *args, **kwargs):
|
|
"""Führt Google API Call mit exponentiellem Backoff bei 403/429 aus"""
|
|
max_retries = 5
|
|
base_delay = 1 # seconds
|
|
|
|
for attempt in range(max_retries):
|
|
try:
|
|
return call_func(*args, **kwargs)
|
|
except HttpError as e:
|
|
if e.resp.status in [403, 429]:
|
|
if attempt == max_retries - 1:
|
|
raise
|
|
delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
|
|
await asyncio.sleep(delay)
|
|
else:
|
|
raise
|
|
|
|
SCOPES = Config.GOOGLE_CALENDAR_SCOPES
|
|
|
|
async def get_google_service(context):
|
|
"""Initialisiert Google Calendar API Service mit Service Account"""
|
|
try:
|
|
service_account_path = Config.GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH
|
|
|
|
if not os.path.exists(service_account_path):
|
|
context.logger.error(f"Service Account Datei nicht gefunden: {service_account_path}")
|
|
context.logger.error("Bitte erstellen Sie einen Google Service Account und legen Sie die service-account.json in das Projekt-Verzeichnis.")
|
|
return None
|
|
|
|
context.logger.info("Initialisiere Google Calendar API mit Service Account...")
|
|
creds = service_account.Credentials.from_service_account_file(
|
|
service_account_path, scopes=SCOPES)
|
|
|
|
service = build('calendar', 'v3', credentials=creds)
|
|
context.logger.info("Google Calendar API erfolgreich initialisiert")
|
|
return service
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler bei Google Service Account Authentifizierung: {e}")
|
|
context.logger.error("Überprüfen Sie die service-account.json Datei und die Berechtigungen.")
|
|
return None
|
|
|
|
async def get_advoware_employees(context):
|
|
"""Ruft alle Mitarbeiter von Advoware ab"""
|
|
advoware = AdvowareAPI(context)
|
|
try:
|
|
# Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere
|
|
result = await advoware.api_call('api/v1/advonet/Mitarbeiter', params={'aktiv': 'true'})
|
|
context.logger.info(f"Advoware Mitarbeiter abgerufen: {len(result) if isinstance(result, list) else 'unbekannt'}")
|
|
return result if isinstance(result, list) else []
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Abrufen der Mitarbeiter: {e}")
|
|
return []
|
|
|
|
async def ensure_google_calendar(service, employee_kuerzel, context):
|
|
"""Stellt sicher, dass ein Google Calendar für den Mitarbeiter existiert"""
|
|
calendar_name = f"AW-{employee_kuerzel}"
|
|
|
|
try:
|
|
# Bestehende Kalender prüfen
|
|
calendar_list = await google_api_call_with_backoff(service.calendarList().list().execute)
|
|
for calendar in calendar_list.get('items', []):
|
|
if calendar['summary'] == calendar_name:
|
|
context.logger.info(f"Google Calendar '{calendar_name}' existiert bereits")
|
|
calendar_id = calendar['id']
|
|
|
|
# Kalender mit Hauptaccount teilen (auch für bestehende)
|
|
acl_rule = {
|
|
'scope': {
|
|
'type': 'user',
|
|
'value': 'lehmannundpartner@gmail.com'
|
|
},
|
|
'role': 'owner'
|
|
}
|
|
try:
|
|
await google_api_call_with_backoff(service.acl().insert(calendarId=calendar_id, body=acl_rule).execute)
|
|
context.logger.info(f"Kalender '{calendar_name}' mit lehmannundpartner@gmail.com geteilt")
|
|
except Exception as e:
|
|
context.logger.info(f"ACL für '{calendar_name}' bereits vorhanden oder Fehler: {e}")
|
|
|
|
return calendar_id
|
|
|
|
# Neuen Kalender erstellen
|
|
calendar_body = {
|
|
'summary': calendar_name,
|
|
'description': f'Advoware Termine für Mitarbeiter {employee_kuerzel}',
|
|
'timeZone': 'Europe/Berlin'
|
|
}
|
|
|
|
created_calendar = await google_api_call_with_backoff(service.calendars().insert(body=calendar_body).execute)
|
|
calendar_id = created_calendar['id']
|
|
context.logger.info(f"Google Calendar '{calendar_name}' erstellt mit ID: {calendar_id}")
|
|
|
|
# Kalender mit Hauptaccount teilen
|
|
acl_rule = {
|
|
'scope': {
|
|
'type': 'user',
|
|
'value': 'lehmannundpartner@gmail.com'
|
|
},
|
|
'role': 'owner'
|
|
}
|
|
await google_api_call_with_backoff(service.acl().insert(calendarId=calendar_id, body=acl_rule).execute)
|
|
context.logger.info(f"Kalender '{calendar_name}' mit lehmannundpartner@gmail.com geteilt")
|
|
|
|
return calendar_id
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler bei Google Calendar für {employee_kuerzel}: {e}")
|
|
return None
|
|
|
|
async def get_advoware_appointments(employee_kuerzel, context):
|
|
"""Ruft Termine eines Mitarbeiters aus Advoware ab"""
|
|
advoware = AdvowareAPI(context)
|
|
|
|
# Zeitraum: aktuelles Jahr + 2 Jahre
|
|
from_date = datetime.datetime.now().strftime('%Y-01-01T00:00:00Z')
|
|
to_date = (datetime.datetime.now() + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
|
|
|
|
try:
|
|
params = {
|
|
'kuerzel': employee_kuerzel,
|
|
'from': from_date,
|
|
'to': to_date
|
|
}
|
|
result = await advoware.api_call('api/v1/advonet/Termine', method='GET', params=params)
|
|
appointments = result if isinstance(result, list) else []
|
|
context.logger.info(f"Advoware Termine für {employee_kuerzel}: {len(appointments)} gefunden")
|
|
return appointments
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Abrufen der Termine für {employee_kuerzel}: {e}")
|
|
return []
|
|
|
|
async def get_google_events(service, calendar_id, context):
|
|
"""Ruft Events aus Google Calendar ab"""
|
|
try:
|
|
now = datetime.datetime.utcnow()
|
|
from_date = now.strftime('%Y-01-01T00:00:00Z')
|
|
to_date = (now + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
|
|
|
|
events_result = await google_api_call_with_backoff(service.events().list(
|
|
calendarId=calendar_id,
|
|
timeMin=from_date,
|
|
timeMax=to_date,
|
|
singleEvents=True,
|
|
orderBy='startTime'
|
|
).execute)
|
|
|
|
events = events_result.get('items', [])
|
|
context.logger.info(f"Google Calendar Events: {len(events)} gefunden")
|
|
return events
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Abrufen der Google Events: {e}")
|
|
return []
|
|
|
|
async def sync_appointment_to_google(service, calendar_id, appointment, full_content, context):
|
|
"""Synchronisiert einen Advoware-Termin zu Google Calendar"""
|
|
try:
|
|
context.logger.info(f"Sync Advoware Termin {appointment.get('frNr')} zu Google starten")
|
|
context.logger.debug(f"Termin Daten: {appointment}")
|
|
|
|
# Advoware sendet bereits datetime strings
|
|
datum = appointment.get('datum', '') # z.B. '2025-05-30T16:30:00'
|
|
datum_bis = appointment.get('datumBis', '') # z.B. '2025-05-30T00:00:00'
|
|
|
|
# Extrahiere Datum und Zeit separat
|
|
if 'T' in datum:
|
|
start_date = datum.split('T')[0] # '2025-05-30'
|
|
start_time = datum.split('T')[1] # '16:30:00'
|
|
else:
|
|
start_date = datum
|
|
start_time = appointment.get('uhrzeitVon') or appointment.get('uhrzeit') or '09:00:00'
|
|
|
|
if 'T' in datum_bis and datum_bis != '0001-01-01T00:00:00': # Advoware default
|
|
end_date = datum_bis.split('T')[0]
|
|
end_time = datum_bis.split('T')[1]
|
|
# Prüfen ob Endzeit 00:00:00 ist (Advoware default für ganztägige Termine)
|
|
if end_time == '00:00:00':
|
|
# Berechne Endzeit als Startzeit + 1 Stunde
|
|
start_hour = int(start_time.split(':')[0])
|
|
end_hour = (start_hour + 1) % 24
|
|
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
|
|
else:
|
|
end_date = start_date
|
|
# Standarddauer: 1 Stunde
|
|
start_hour = int(start_time.split(':')[0])
|
|
end_hour = (start_hour + 1) % 24
|
|
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
|
|
|
|
# Vollständiges Event oder nur "blocked" - immer "blocked"
|
|
summary = "Advoware blocked"
|
|
description = f"Advoware Termin ID: {appointment.get('frNr')}"
|
|
location = ""
|
|
|
|
# Google Calendar erwartet RFC3339 formatierte Datetimes
|
|
start_datetime = f"{start_date}T{start_time}+01:00" # +01:00 für Europe/Berlin
|
|
end_datetime = f"{end_date}T{end_time}+01:00"
|
|
|
|
context.logger.info(f"Erstelle Google Event: Start {start_datetime}, End {end_datetime}")
|
|
|
|
event_body = {
|
|
'summary': summary,
|
|
'description': description,
|
|
'location': location,
|
|
'start': {
|
|
'dateTime': start_datetime,
|
|
'timeZone': 'Europe/Berlin',
|
|
},
|
|
'end': {
|
|
'dateTime': end_datetime,
|
|
'timeZone': 'Europe/Berlin',
|
|
},
|
|
'extendedProperties': {
|
|
'private': {
|
|
'advoware_frnr': str(appointment.get('frNr'))
|
|
}
|
|
}
|
|
}
|
|
|
|
# Event erstellen
|
|
context.logger.info("Sende Google Calendar Insert Request...")
|
|
created_event = await google_api_call_with_backoff(service.events().insert(calendarId=calendar_id, body=event_body).execute)
|
|
context.logger.info(f"Termin {appointment.get('frNr')} zu Google Calendar hinzugefügt, Event ID: {created_event.get('id')}")
|
|
return created_event
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Sync zu Google für Termin {appointment.get('frNr')}: {e}")
|
|
return None
|
|
|
|
async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel, context):
|
|
"""Synchronisiert ein Google Event zu Advoware (nur wenn change_allowed_token validiert)"""
|
|
try:
|
|
context.logger.info(f"Sync Google Event {event.get('id')} zu Advoware starten: {event.get('summary')}")
|
|
context.logger.debug(f"Event Daten: {event}")
|
|
|
|
# Prüfen ob bereits eine frNr vorhanden (aus description parsen)
|
|
description = event.get('description', '')
|
|
frnr = None
|
|
change_token = None
|
|
if 'frNr: ' in description and 'sync-token: ' in description:
|
|
frnr_parts = description.split('frNr: ')
|
|
if len(frnr_parts) > 1:
|
|
frnr = frnr_parts[1].split('\n')[0]
|
|
token_parts = description.split('sync-token: ')
|
|
if len(token_parts) > 1:
|
|
change_token = token_parts[1].split('\n')[0]
|
|
context.logger.info(f"Parsed frNr: {frnr}, Token: {change_token}")
|
|
|
|
if frnr and change_token:
|
|
# Validierung des Tokens
|
|
expected_token = generate_change_token(frnr)
|
|
if change_token != expected_token:
|
|
context.logger.info(f"Token für Google Event {event.get('id')} nicht validiert, überspringe Update")
|
|
return None
|
|
|
|
# Termin aus Advoware abrufen zum Vergleich
|
|
advoware = AdvowareAPI(context)
|
|
result = await advoware.api_call('api/v1/advonet/Termine', params={'frnr': frnr})
|
|
existing_appointment = result[0] if result and isinstance(result, list) and len(result) > 0 else None
|
|
if not existing_appointment:
|
|
context.logger.error(f"Termin {frnr} nicht in Advoware gefunden")
|
|
return None
|
|
|
|
# Daten aus Google Event extrahieren
|
|
start = event.get('start', {}).get('dateTime') or event.get('start', {}).get('date', '')
|
|
end = event.get('end', {}).get('dateTime') or event.get('end', {}).get('date', '')
|
|
summary = event.get('summary', '')
|
|
description = event.get('description', '')
|
|
location = event.get('location', '')
|
|
|
|
# Mit Advoware-Daten vergleichen
|
|
advoware_datum = existing_appointment.get('datum', '')
|
|
advoware_uhrzeit_bis = existing_appointment.get('uhrzeitBis', '')
|
|
advoware_text = existing_appointment.get('text', '')
|
|
advoware_notiz = existing_appointment.get('notiz', '')
|
|
advoware_ort = existing_appointment.get('ort', '')
|
|
|
|
# Berechne erwartete Werte aus Google
|
|
expected_datum = start.split('+')[0] if '+' in start else start
|
|
expected_uhrzeit_bis = end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00'
|
|
# Für all-day Events (nur date, keine Zeit) setze Default-Start-Zeit
|
|
if 'T' not in start:
|
|
expected_datum = f"{start}T09:00:00"
|
|
context.logger.info(f"All-day Event erkannt, setze Default-Start-Zeit auf 09:00:00")
|
|
expected_text = summary
|
|
expected_notiz = description
|
|
expected_ort = location
|
|
|
|
context.logger.debug(f"Vergleiche: Advoware datum={advoware_datum}, expected={expected_datum}")
|
|
context.logger.debug(f"Vergleiche: Advoware uhrzeit_bis={advoware_uhrzeit_bis}, expected={expected_uhrzeit_bis}")
|
|
context.logger.debug(f"Vergleiche: Advoware text={advoware_text}, expected={expected_text}")
|
|
context.logger.debug(f"Vergleiche: Advoware notiz={advoware_notiz}, expected={expected_notiz}")
|
|
context.logger.debug(f"Vergleiche: Advoware ort={advoware_ort}, expected={expected_ort}")
|
|
|
|
if (advoware_datum == expected_datum and
|
|
advoware_uhrzeit_bis == expected_uhrzeit_bis and
|
|
advoware_text == expected_text and
|
|
advoware_notiz == expected_notiz and
|
|
advoware_ort == expected_ort):
|
|
context.logger.info(f"Google Event {event.get('id')} unverändert, überspringe Advoware Update")
|
|
return None
|
|
|
|
context.logger.info(f"Termin {frnr} geändert, aktualisiere in Advoware...")
|
|
|
|
# Update Termin in Advoware
|
|
# Kein Anhängen von sync_info bei bestehenden Terminen
|
|
updated_description = description
|
|
|
|
appointment_data = {
|
|
'frNr': int(frnr),
|
|
'text': summary,
|
|
'notiz': updated_description,
|
|
'ort': location,
|
|
'datum': start.split('+')[0] if '+' in start else start,
|
|
'uhrzeitBis': end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
|
|
'datumBis': end.split('+')[0] if '+' in end else end,
|
|
'sb': employee_kuerzel,
|
|
'anwalt': employee_kuerzel,
|
|
'vorbereitungsDauer': '00:00:00'
|
|
}
|
|
await update_advoware_appointment(advoware, frnr, change_token, appointment_data, context)
|
|
return frnr
|
|
else:
|
|
context.logger.info(f"Neuer Termin aus Google, erstelle in Advoware...")
|
|
# Neuen Termin in Advoware erstellen
|
|
advoware = AdvowareAPI(context)
|
|
|
|
# Start/End aus Google Event extrahieren
|
|
start = event.get('start', {}).get('dateTime') or event.get('start', {}).get('date', '')
|
|
end = event.get('end', {}).get('dateTime') or event.get('end', {}).get('date', '')
|
|
|
|
# Advoware-Termin erstellen
|
|
appointment_data = {
|
|
'text': event.get('summary', 'Google Calendar Termin'),
|
|
'notiz': event.get('description', ''),
|
|
'ort': event.get('location', ''),
|
|
'datum': start.split('+')[0] if '+' in start else start,
|
|
'uhrzeitBis': end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
|
|
'datumBis': end.split('+')[0] if '+' in end else end,
|
|
'sb': employee_kuerzel,
|
|
'anwalt': employee_kuerzel,
|
|
'vorbereitungsDauer': '00:00:00'
|
|
}
|
|
|
|
context.logger.info("Sende Advoware POST Request...")
|
|
result = await advoware.api_call('api/v1/advonet/Termine', method='POST', json_data=appointment_data)
|
|
|
|
context.logger.info(f"POST Termine Response: {result}")
|
|
|
|
if result and isinstance(result, dict):
|
|
new_frnr = result.get('frNr') or result.get('frnr')
|
|
if new_frnr:
|
|
context.logger.info(f"Neuer Advoware Termin erstellt: {new_frnr}")
|
|
# Sync-Token berechnen
|
|
token = generate_change_token(new_frnr)
|
|
|
|
# Sync-Info für beide Systeme vorbereiten
|
|
sync_info = f"\n\n## no change below this line ##\nfrNr: {new_frnr}\nsync-token: {token}"
|
|
existing_description = event.get('description', '')
|
|
new_description = existing_description + sync_info
|
|
|
|
# Sofort frNr und sync-token in Advoware schreiben (PUT)
|
|
context.logger.info(f"Schreibe sync-info in Advoware Termin {new_frnr}...")
|
|
update_data = {'frNr': int(new_frnr), 'notiz': new_description}
|
|
await advoware.api_call('api/v1/advonet/Termine', method='PUT', json_data=update_data)
|
|
context.logger.info(f"Sync-info in Advoware eingefügt")
|
|
|
|
# Sofort frNr und sync-token in Google schreiben
|
|
context.logger.info(f"Schreibe sync-info in Google Event {event.get('id')}...")
|
|
event['description'] = new_description
|
|
|
|
await google_api_call_with_backoff(service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute)
|
|
context.logger.info(f"Sync-info in Google aktualisiert")
|
|
|
|
return new_frnr
|
|
else:
|
|
context.logger.error(f"Keine frNr in POST Response: {result}")
|
|
else:
|
|
context.logger.error(f"Ungültige POST Response: {result}")
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Sync zu Advoware für Google Event {event.get('id')}: {e}")
|
|
return None
|
|
|
|
async def update_google_event(service, calendar_id, existing_event, appointment, context):
|
|
"""Aktualisiert ein bestehendes Google Event mit Advoware-Daten, nur wenn Änderungen vorliegen"""
|
|
try:
|
|
context.logger.info(f"Aktualisiere Google Event für Advoware Termin {appointment.get('frNr')}")
|
|
context.logger.debug(f"Appointment Daten: {appointment}")
|
|
|
|
# Gleiche Logik wie in sync_appointment_to_google für datetime und body
|
|
datum = appointment.get('datum', '')
|
|
datum_bis = appointment.get('datumBis', '')
|
|
|
|
if 'T' in datum:
|
|
start_date = datum.split('T')[0]
|
|
start_time = datum.split('T')[1]
|
|
else:
|
|
start_date = datum
|
|
start_time = appointment.get('uhrzeitVon') or appointment.get('uhrzeit') or '09:00:00'
|
|
|
|
if 'T' in datum_bis and datum_bis != '0001-01-01T00:00:00':
|
|
end_date = datum_bis.split('T')[0]
|
|
end_time = datum_bis.split('T')[1]
|
|
if end_time == '00:00:00':
|
|
start_hour = int(start_time.split(':')[0])
|
|
end_hour = (start_hour + 1) % 24
|
|
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
|
|
else:
|
|
end_date = start_date
|
|
start_hour = int(start_time.split(':')[0])
|
|
end_hour = (start_hour + 1) % 24
|
|
end_time = f"{end_hour:02d}:{start_time.split(':')[1]}:{start_time.split(':')[2]}"
|
|
|
|
summary = "Advoware blocked"
|
|
description = f"Advoware Termin ID: {appointment.get('frNr')}"
|
|
location = ""
|
|
|
|
start_datetime = f"{start_date}T{start_time}+01:00"
|
|
end_datetime = f"{end_date}T{end_time}+01:00"
|
|
|
|
# Prüfe ob es ein Google-initiales Event ist (hat sync-token in description)
|
|
is_google_initial = 'sync-token: ' in existing_event.get('description', '')
|
|
if is_google_initial:
|
|
# Behalte die originale Summary
|
|
summary = existing_event.get('summary', summary)
|
|
context.logger.debug(f"Google-initiales Event, behalte Summary: {summary}")
|
|
|
|
# Vergleiche mit bestehendem Event
|
|
current_start = existing_event.get('start', {}).get('dateTime', '')
|
|
current_end = existing_event.get('end', {}).get('dateTime', '')
|
|
current_summary = existing_event.get('summary', '')
|
|
current_description = existing_event.get('description', '')
|
|
current_location = existing_event.get('location', '')
|
|
|
|
context.logger.debug(f"Vergleiche Start: {current_start} vs {start_datetime}")
|
|
context.logger.debug(f"Vergleiche End: {current_end} vs {end_datetime}")
|
|
context.logger.debug(f"Vergleiche Summary: {current_summary} vs {summary}")
|
|
context.logger.debug(f"Vergleiche Description: {current_description} vs {description}")
|
|
context.logger.debug(f"Vergleiche Location: {current_location} vs {location}")
|
|
|
|
if (current_start == start_datetime and
|
|
current_end == end_datetime and
|
|
current_summary == summary and
|
|
current_description == description and
|
|
current_location == location):
|
|
context.logger.info(f"Termin {appointment.get('frNr')} unverändert, überspringe Google Update")
|
|
return None
|
|
|
|
context.logger.info(f"Termin {appointment.get('frNr')} geändert, aktualisiere in Google...")
|
|
|
|
event_body = {
|
|
'summary': summary,
|
|
'description': description,
|
|
'location': location,
|
|
'start': {
|
|
'dateTime': start_datetime,
|
|
'timeZone': 'Europe/Berlin',
|
|
},
|
|
'end': {
|
|
'dateTime': end_datetime,
|
|
'timeZone': 'Europe/Berlin',
|
|
}
|
|
}
|
|
|
|
# Event aktualisieren
|
|
context.logger.info("Sende Google Calendar Update Request...")
|
|
updated_event = await google_api_call_with_backoff(service.events().update(calendarId=calendar_id, eventId=existing_event['id'], body=event_body).execute)
|
|
context.logger.info(f"Termin {appointment.get('frNr')} in Google Calendar aktualisiert")
|
|
return updated_event
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Update des Google Events für Termin {appointment.get('frNr')}: {e}")
|
|
return None
|
|
|
|
async def update_advoware_appointment(advoware, frnr, change_token, data, context):
|
|
"""Aktualisiert einen Advoware-Termin mit Token-Validierung"""
|
|
context.logger.info(f"Aktualisiere Advoware Termin {frnr}...")
|
|
expected_token = generate_change_token(frnr)
|
|
if change_token != expected_token:
|
|
context.logger.error(f"Token-Validierung fehlgeschlagen für Update von Termin {frnr}: expected {expected_token}, got {change_token}")
|
|
raise ValueError("Invalid change token")
|
|
|
|
# Entferne frNr aus data, da es in der URL ist
|
|
data_copy = data.copy()
|
|
context.logger.debug(f"Sende PUT Request mit Daten: {data_copy}")
|
|
result = await advoware.api_call('api/v1/advonet/Termine', method='PUT', json_data=data_copy)
|
|
context.logger.info(f"Advoware Termin {frnr} aktualisiert")
|
|
return result
|
|
|
|
async def delete_advoware_appointment(advoware, frnr, change_token, context):
|
|
"""Löscht einen Advoware-Termin mit Token-Validierung"""
|
|
context.logger.info(f"Lösche Advoware Termin {frnr}...")
|
|
expected_token = generate_change_token(frnr)
|
|
if change_token != expected_token:
|
|
context.logger.error(f"Token-Validierung fehlgeschlagen für Löschung von Termin {frnr}: expected {expected_token}, got {change_token}")
|
|
raise ValueError("Invalid change token")
|
|
|
|
context.logger.debug("Sende DELETE Request...")
|
|
result = await advoware.api_call('api/v1/advonet/Termine', method='DELETE', params={'frnr': frnr})
|
|
context.logger.info(f"Advoware Termin {frnr} gelöscht")
|
|
return result
|
|
|
|
async def handler(event, context):
|
|
try:
|
|
# Konfiguration aus Event-Daten
|
|
data = event.get('data', {})
|
|
body = data.get('body', {})
|
|
full_content = body.get('full_content', True) # Default: volle Termindetails
|
|
|
|
context.logger.info(f"=== Calendar Sync Event gestartet ===")
|
|
context.logger.info(f"Event Data: {data}")
|
|
context.logger.info(f"Full Content: {full_content}, Triggered By: {body.get('triggered_by')}")
|
|
|
|
# Google Calendar Service initialisieren
|
|
context.logger.info("Initialisiere Google Calendar Service...")
|
|
service = await get_google_service(context)
|
|
if not service:
|
|
context.logger.warn("Google Calendar Service nicht verfügbar. Sync wird übersprungen.")
|
|
return {
|
|
'status': 200,
|
|
'body': {
|
|
'status': 'skipped',
|
|
'reason': 'Google Calendar credentials not configured',
|
|
'total_synced': 0
|
|
}
|
|
}
|
|
|
|
# Alle Mitarbeiter abrufen
|
|
context.logger.info("Rufe Advoware Mitarbeiter ab...")
|
|
employees = await get_advoware_employees(context)
|
|
|
|
if not employees:
|
|
context.logger.error("Keine Mitarbeiter gefunden. Sync abgebrochen.")
|
|
return {'status': 500, 'body': {'error': 'Keine Mitarbeiter gefunden'}}
|
|
|
|
total_synced = 0
|
|
deleted_frnrs = set() # Sammle frNr von gelöschten Terminen
|
|
|
|
for employee in employees:
|
|
kuerzel = employee.get('kuerzel') or employee.get('anwalt')
|
|
if not kuerzel:
|
|
context.logger.warn(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
|
|
continue
|
|
|
|
# Zum Testen nur "AI" synchronisieren
|
|
if kuerzel != 'AI':
|
|
context.logger.info(f"Mitarbeiter {kuerzel} übersprungen (nur AI im Testmodus)")
|
|
continue
|
|
|
|
context.logger.info(f"=== Verarbeite Mitarbeiter: {kuerzel} ===")
|
|
|
|
# Google Calendar sicherstellen
|
|
calendar_id = await ensure_google_calendar(service, kuerzel, context)
|
|
if not calendar_id:
|
|
context.logger.error(f"Google Calendar für {kuerzel} konnte nicht erstellt werden. Überspringe.")
|
|
continue
|
|
|
|
# Termine aus beiden Systemen abrufen
|
|
context.logger.info(f"Rufe Advoware Termine für {kuerzel} ab...")
|
|
advoware_appointments = await get_advoware_appointments(kuerzel, context)
|
|
context.logger.info(f"Rufe Google Events für {kuerzel} ab...")
|
|
google_events = await get_google_events(service, calendar_id, context)
|
|
|
|
# Advoware → Google syncen
|
|
context.logger.info("=== Advoware → Google Sync starten ===")
|
|
frnr_to_event = {}
|
|
for event in google_events:
|
|
desc = event.get('description', '')
|
|
if 'frNr: ' in desc:
|
|
parts = desc.split('frNr: ')
|
|
if len(parts) > 1:
|
|
frnr = parts[1].split('\n')[0]
|
|
frnr_to_event[frnr] = event
|
|
context.logger.debug(f"Google Event {event.get('id')} zu frNr {frnr} zugeordnet")
|
|
|
|
for appointment in advoware_appointments:
|
|
frnr = str(appointment.get('frNr'))
|
|
if frnr in deleted_frnrs:
|
|
context.logger.info(f"Termin {frnr} wurde gerade gelöscht, überspringe Google Sync")
|
|
continue
|
|
if frnr in frnr_to_event:
|
|
# Termin existiert bereits, aktualisieren
|
|
context.logger.info(f"Termin {frnr} existiert in Google, aktualisiere...")
|
|
existing_event = frnr_to_event[frnr]
|
|
await update_google_event(service, calendar_id, existing_event, appointment, context)
|
|
total_synced += 1
|
|
else:
|
|
# Neuen Termin erstellen
|
|
context.logger.info(f"Termin {frnr} neu in Advoware, erstelle in Google...")
|
|
await sync_appointment_to_google(service, calendar_id, appointment, full_content, context)
|
|
total_synced += 1
|
|
|
|
# Google → Advoware syncen
|
|
context.logger.info("=== Google → Advoware Sync starten ===")
|
|
for event in google_events:
|
|
context.logger.info(f"Verarbeite Google Event {event.get('id')}: {event.get('summary')}")
|
|
await sync_event_to_advoware(service, calendar_id, event, kuerzel, context)
|
|
|
|
# Löschungen handhaben: Google-initiale Termine, die in Google gelöscht wurden
|
|
context.logger.info("=== Löschungen behandeln ===")
|
|
google_frnrs_with_token = set()
|
|
for event in google_events:
|
|
desc = event.get('description', '')
|
|
if 'frNr: ' in desc and 'sync-token: ' in desc:
|
|
frnr_parts = desc.split('frNr: ')
|
|
if len(frnr_parts) > 1:
|
|
frnr = frnr_parts[1].split('\n')[0]
|
|
google_frnrs_with_token.add(frnr)
|
|
context.logger.debug(f"Google Event mit Token gefunden: frNr {frnr}")
|
|
|
|
for appointment in advoware_appointments:
|
|
notiz = appointment.get('notiz', '')
|
|
if '## no change below this line ##' in notiz and 'sync-token: ' in notiz:
|
|
frnr = str(appointment.get('frNr'))
|
|
# Token aus notiz extrahieren
|
|
parts = notiz.split('sync-token: ')
|
|
if len(parts) > 1:
|
|
stored_token = parts[1].split('\n')[0]
|
|
expected_token = generate_change_token(frnr)
|
|
context.logger.debug(f"Prüfe Löschung für Termin {frnr}: stored_token={stored_token}, expected={expected_token}, in_google={frnr in google_frnrs_with_token}")
|
|
if stored_token == expected_token and frnr not in google_frnrs_with_token:
|
|
# Lösche in Advoware
|
|
context.logger.info(f"Termin {frnr} in Google gelöscht, lösche in Advoware...")
|
|
advoware = AdvowareAPI(context)
|
|
await delete_advoware_appointment(advoware, frnr, stored_token, context)
|
|
deleted_frnrs.add(frnr)
|
|
else:
|
|
context.logger.debug(f"Termin {frnr} nicht gelöscht: Token mismatch oder noch in Google")
|
|
else:
|
|
context.logger.warn(f"Ungültiger sync-token in Termin {frnr}")
|
|
else:
|
|
context.logger.debug(f"Termin {frnr} hat keinen Token, überspringe Löschprüfung")
|
|
|
|
context.logger.info(f"=== Advoware Calendar Sync abgeschlossen. {total_synced} Termine synchronisiert. Gelöschte Termine: {len(deleted_frnrs)} ===")
|
|
|
|
return {
|
|
'status': 200,
|
|
'body': {
|
|
'status': 'completed',
|
|
'total_synced': total_synced,
|
|
'employees_processed': len([e for e in employees if e.get('kuerzel') or e.get('anwalt')]),
|
|
'deleted_appointments': len(deleted_frnrs)
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
context.logger.error(f"Fehler beim Advoware Calendar Sync: {e}")
|
|
return {
|
|
'status': 500,
|
|
'body': {
|
|
'error': 'Internal server error',
|
|
'details': str(e)
|
|
}
|
|
} |