diff --git a/bitbylaw/config.py b/bitbylaw/config.py index 7a2a24fb..ab2ae933 100644 --- a/bitbylaw/config.py +++ b/bitbylaw/config.py @@ -37,4 +37,4 @@ class Config: # Calendar Sync settings CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS = os.getenv('CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS', 'true').lower() == 'true' - ADVOWARE_WRITE_PROTECTION = os.getenv('ADVOWARE_WRITE_PROTECTION', 'false').lower() == 'true' \ No newline at end of file + ADVOWARE_WRITE_PROTECTION = True \ No newline at end of file diff --git a/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py b/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py index 841d98d9..734c9cff 100644 --- a/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_cron_step.py @@ -12,7 +12,7 @@ config = { 'emits': ['calendar.sync.triggered'] } -async def handler(event, context): +async def handler(context): try: # Prüfe ob bereits ein Sync läuft redis_client = redis.Redis( diff --git a/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py b/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py index 620f5b9c..a8c1476a 100644 --- a/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py @@ -29,7 +29,7 @@ FETCH_TO = (datetime.datetime.now(BERLIN_TZ) + timedelta(days=730)).strftime('%Y CALENDAR_SYNC_LOCK_KEY = 'calendar_sync_lock' # Relevant fields for data comparison (simple diff) -COMPARISON_FIELDS = ['text', 'notiz', 'ort', 'datum', 'uhrzeitBis', 'datumBis', 'dauertermin', 'turnus', 'turnusArt'] +COMPARISON_FIELDS = ['text', 'notiz', 'ort', 'datum', 'uhrzeitBis', 'datumBis', 'dauertermin', 'turnus', 'turnusArt', 'recurrence'] async def connect_db(): """Connect to Postgres DB from Config.""" @@ -91,6 +91,23 @@ async def ensure_google_calendar(service, employee_kuerzel): logger.error(f"Failed to ensure Google calendar for {employee_kuerzel}: {e}") raise +async def share_google_calendar(service, calendar_id, email): + """Share Google Calendar with specific email.""" + try: + acl_rule = { + 'scope': {'type': 'user', 'value': email}, + 'role': 'owner' + } + result = service.acl().insert(calendarId=calendar_id, body=acl_rule).execute() + logger.info(f"Shared calendar {calendar_id} with {email}") + return result + except HttpError as e: + logger.error(f"Google API error sharing calendar {calendar_id} with {email}: {e}") + raise + except Exception as e: + logger.error(f"Failed to share Google calendar {calendar_id} with {email}: {e}") + raise + async def fetch_advoware_appointments(advoware, employee_kuerzel): """Fetch Advoware appointments in range.""" try: @@ -135,6 +152,39 @@ async def fetch_google_events(service, calendar_id): logger.error(f"Failed to fetch Google events: {e}") raise +def generate_rrule(turnus, turnus_art, datum_bis): + """Generate RRULE string from Advoware turnus and turnusArt.""" + freq_map = { + 1: 'DAILY', + 2: 'WEEKLY', + 3: 'MONTHLY', + 4: 'YEARLY' + } + if turnus_art not in freq_map: + return None + freq = freq_map[turnus_art] + + # Parse datum_bis to date and limit to max 2 years from now to avoid Google Calendar limits + try: + if 'T' in datum_bis: + bis_dt = datetime.datetime.fromisoformat(datum_bis.replace('Z', '')) + else: + bis_dt = datetime.datetime.fromisoformat(datum_bis + 'T00:00:00') + + # Limit to max 2 years from now + max_until = datetime.datetime.now() + timedelta(days=730) # 2 years + if bis_dt > max_until: + bis_dt = max_until + logger.info(f"Limited recurrence until date to {bis_dt.date()} to avoid Google Calendar limits") + + until_date = bis_dt.strftime('%Y%m%d') + except ValueError: + logger.warning(f"Invalid datum_bis: {datum_bis}, skipping recurrence") + return None + + rrule = f"RRULE:FREQ={freq};INTERVAL={turnus};UNTIL={until_date}" + return rrule + def standardize_appointment_data(data, source): """Standardize data from Advoware or Google to comparable dict, with TZ handling.""" if source == 'advoware': @@ -173,6 +223,17 @@ def standardize_appointment_data(data, source): notiz = data.get('notiz', '') ort = data.get('ort', '') + # Generate recurrence if dauertermin + recurrence = None + if data.get('dauertermin', 0) == 1: + turnus = data.get('turnus', 1) + turnus_art = data.get('turnusArt', 1) + datum_bis = data.get('datumBis', '') + if datum_bis: + recurrence = generate_rrule(turnus, turnus_art, datum_bis) + if recurrence: + recurrence = [recurrence] # Google expects list of strings + return { 'start': start_dt, 'end': end_dt, @@ -182,7 +243,7 @@ def standardize_appointment_data(data, source): 'dauertermin': data.get('dauertermin', 0), 'turnus': data.get('turnus', 0), 'turnusArt': data.get('turnusArt', 0), - 'recurrence': None # No RRULE in Advoware + 'recurrence': recurrence } elif source == 'google': start_obj = data.get('start', {}) @@ -432,6 +493,11 @@ async def handler(event, context): logger.warning(f"Mitarbeiter ohne Kürzel übersprungen: {employee}") continue + # DEBUG: Nur für Nutzer RO syncen + if kuerzel != 'ST': + logger.info(f"DEBUG: Überspringe {kuerzel}, nur RO wird gesynct") + continue + logger.info(f"Starting calendar sync for {kuerzel}") redis_client = redis.Redis(