diff --git a/bitbylaw/steps/advoware_cal_sync/calendar_sync_api_step.py b/bitbylaw/steps/advoware_cal_sync/calendar_sync_api_step.py new file mode 100644 index 00000000..19770578 --- /dev/null +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_api_step.py @@ -0,0 +1,49 @@ +import json + +config = { + 'type': 'api', + 'name': 'Calendar Sync API Trigger', + 'description': 'API-Endpunkt zum manuellen Auslösen des Calendar Sync', + 'path': '/advoware/calendar/sync', + 'method': 'POST', + 'emits': ['calendar.sync.triggered'] +} + +async def handler(req, context): + try: + # Konfiguration aus Request-Body + body = req.get('body', {}) + full_content = body.get('full_content', True) + + print(f"Calendar Sync API aufgerufen, full_content: {full_content}") + + # Emit Event für den Sync + await context.emit({ + "topic": "calendar.sync.triggered", + "data": { + "body": { + "full_content": full_content, + "triggered_by": "api" + } + } + }) + + return { + 'status': 200, + 'body': { + 'status': 'triggered', + 'message': 'Calendar sync wurde ausgelöst', + 'full_content': full_content, + 'triggered_by': 'api' + } + } + + except Exception as e: + context.logger.error(f"Fehler beim API-Trigger: {e}") + return { + 'status': 500, + 'body': { + 'error': 'Internal server error', + 'details': str(e) + } + } diff --git a/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py b/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py new file mode 100644 index 00000000..dd8ce99d --- /dev/null +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py @@ -0,0 +1,37 @@ +import json + +config = { + 'type': 'cron', + 'name': 'Calendar Sync Cron Job', + 'description': 'Führt den Calendar Sync alle 15 Minuten automatisch aus', + 'cron': '*/15 * * * *', # Alle 15 Minuten + 'emits': ['calendar.sync.triggered'] +} + +async def handler(event, context): + try: + context.logger.info("Calendar Sync Cron: Starte automatische Synchronisation alle 15 Minuten") + + # Emit Event für den Sync + await context.emit({ + "topic": "calendar.sync.triggered", + "data": { + "body": { + "full_content": True, # Cron verwendet immer volle Details + "triggered_by": "cron" + } + } + }) + + context.logger.info("Calendar Sync Cron: Event wurde emittiert") + return { + 'status': 'completed', + 'triggered_by': 'cron' + } + + except Exception as e: + context.logger.error(f"Fehler beim Cron-Job: {e}") + return { + 'status': 'error', + 'error': str(e) + } diff --git a/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py b/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py new file mode 100644 index 00000000..9ca84bad --- /dev/null +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py @@ -0,0 +1,684 @@ +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) + } + } \ No newline at end of file