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) } }