diff --git a/bitbylaw/steps/advoware_cal_sync/README.md b/bitbylaw/steps/advoware_cal_sync/README.md index de2e7a35..3bca4763 100644 --- a/bitbylaw/steps/advoware_cal_sync/README.md +++ b/bitbylaw/steps/advoware_cal_sync/README.md @@ -143,6 +143,11 @@ Beide Systeme werden auf gemeinsames Format normalisiert (Berlin TZ): - **API-Zuverlässigkeit**: Manchmal erfolgreicher POST, aber `frNr: None` (trotz gültiger Response). 500-Fehler bei Bad Requests. Keine Timestamp-Details in Responses. - **Zeitzonen**: Alles implizit Berlin; Code konvertiert explizit. - **Andere Bugs**: `zuletztGeaendertAm` für Timestamps, aber Format unzuverlässig. +- **DELETE Responses**: DELETE-Anfragen geben manchmal einen leeren Body zurück, was zu `JSONDecodeError` führt. Code fängt dies mit try/except ab und gibt `None` zurück, um den Sync nicht zu brechen. +- **frNr Wiederverwendung**: frNr sind sequentiell und werden nicht wiederverwendet. Getestet durch Erstellen/Löschen/Erstellen: z.B. 85861, 85862, delete 85861, nächstes Create 85863. Kein Risiko für DB-Konflikte durch ID-Reuse. +- **Timestamp-basierte Updates**: Um Race-Conditions und redundante Syncs zu vermeiden, werden Updates in Phase 4 nur durchgeführt, wenn der API-Timestamp der Quelle > `last_sync` (gesetzt auf den API-Timestamp nach erfolgreichem Write). +- **Soft Deletes und Partielle Unique Indexes**: Gelöschte Termine werden mit `deleted = TRUE` markiert, nicht entfernt. Partielle Unique Indexes (z.B. `WHERE deleted = FALSE`) verhindern Duplikate für aktive Einträge. +- **Anonymisierung**: Optionale Anonymisierung sensibler Daten (Text, Notiz, Ort) bei Advoware → Google Sync, um Datenschutz zu wahren (z.B. `text='Advoware blocked'`). ### Google Calendar API (Zuverlässig) - **Zeitformate**: `dateTime` als ISO mit TZ (z.B. `'2025-01-01T10:00:00+01:00'`), `date` für All-Day. Code parst mit `fromisoformat` und `.rstrip('Z')`. @@ -253,465 +258,3 @@ Cron-Step für regelmäßige Ausführung. ## Erweiterungen -- Inkrementelle Syncs basierend auf `last_sync`. -- Mehr Strategien (z.B. 'manual'). -- Webhooks für Echtzeit. -- Tests und Monitoring. - -## Übersicht - -Das System synchronisiert Termine zwischen: -- **Advoware**: Zentrale Terminverwaltung mit detaillierten Informationen (aber vielen API-Bugs). -- **Google Calendar**: Benutzerfreundliche Kalenderansicht für jeden Mitarbeiter. -- **PostgreSQL Hub**: Zentraler Datenspeicher für State, Policies und Audit-Logs. - -## Architektur - -### Hub-Design -- **Single Source of Truth**: Alle Sync-Informationen werden in PostgreSQL gespeichert. -- **Policies**: Enums für Sync-Strategien (`source_system_wins`, `last_change_wins`) und Flags für Schreibberechtigung (`advoware_write_allowed`). -- **Status-Tracking**: `sync_status` ('pending', 'synced', 'failed') für Monitoring und Retries. -- **Transaktionen**: Jede DB-Operation läuft in separaten Transaktionen; Fehler beeinflussen nur den aktuellen Eintrag. -- **Soft Deletes**: Gelöschte Termine werden markiert, nicht entfernt. -- **Phasen-basierte Verarbeitung**: Sync in 4 Phasen, um Neue, Deletes und Updates zu trennen. - -### Sync-Phasen -1. **Phase 1: Neue Einträge Advoware → Google** - Erstelle Google-Events für neue Advoware-Termine, dann DB-Insert. -2. **Phase 2: Neue Einträge Google → Advoware** - Erstelle Advoware-Termine für neue Google-Events, dann DB-Insert. -3. **Phase 3: Gelöschte Einträge identifizieren** - Handle Deletes/Recreates basierend auf Strategie. -4. **Phase 4: Bestehende Einträge updaten** - Update bei Änderungen, basierend auf Timestamps. - -### Datenbank-Schema -```sql --- Haupt-Tabelle -CREATE TABLE calendar_sync ( - sync_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - employee_kuerzel VARCHAR(10) NOT NULL, - advoware_frnr INTEGER, - google_event_id VARCHAR(255), - source_system source_system_enum NOT NULL, - sync_strategy sync_strategy_enum NOT NULL DEFAULT 'source_system_wins', - sync_status sync_status_enum NOT NULL DEFAULT 'synced', - advoware_write_allowed BOOLEAN NOT NULL DEFAULT FALSE, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - last_sync TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Enums -CREATE TYPE source_system_enum AS ENUM ('advoware', 'google'); -CREATE TYPE sync_strategy_enum AS ENUM ('source_system_wins', 'last_change_wins'); -CREATE TYPE sync_status_enum AS ENUM ('pending', 'synced', 'failed'); - --- Audit-Tabelle -CREATE TABLE calendar_sync_audit ( - id SERIAL PRIMARY KEY, - action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE - timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Indizes (angepasst für Soft Deletes) -CREATE UNIQUE INDEX idx_calendar_sync_advoware ON calendar_sync (employee_kuerzel, advoware_frnr) WHERE advoware_frnr IS NOT NULL AND deleted = FALSE; -CREATE UNIQUE INDEX idx_calendar_sync_google ON calendar_sync (employee_kuerzel, google_event_id) WHERE google_event_id IS NOT NULL; -``` - -## Funktionalität - -### Automatische Kalender-Erstellung -- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt. -- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB". -- Kalender wird mit dem Haupt-Google-Account (`lehmannundpartner@gmail.com`) als Owner geteilt. - -### Phasen-Details - -#### Phase 1: Neue Einträge Advoware → Google -- Fetch Advoware-Termine. -- Für jede frNr, die nicht in DB (deleted=FALSE) existiert: Standardisiere Daten, erstelle Google-Event, dann INSERT in DB mit `sync_status = 'synced'`. -- Bei Fehlern: Warnung loggen, weitermachen (nicht abbrechen). - -#### Phase 2: Neue Einträge Google → Advoware -- Fetch Google-Events. -- Für jeden event_id, der nicht in DB existiert: Standardisiere Daten, erstelle Advoware-Termin, dann INSERT in DB mit `sync_status = 'synced'`. -- Bei frNr None (API-Bug): Skippen mit Warnung. -- Bei Fehlern: Warnung loggen, weitermachen. - -#### Phase 3: Gelöschte Einträge identifizieren -- Für jeden DB-Eintrag: Prüfe, ob Termin in API fehlt. -- Bei beiden fehlend: Soft Delete. -- Bei einem fehlend: Recreate oder propagate Delete basierend auf Strategie. -- Bei Fehlern: `sync_status = 'failed'`, Warnung. - -#### Phase 4: Bestehende Einträge updaten -- Für bestehende Einträge: Vergleiche standardisierte Daten. -- Bei Differenzen: Update basierend auf Strategie und Timestamps. -- Bei Fehlern: `sync_status = 'failed'`, Warnung. - -### Datenmapping und Standardisierung -Beide Systeme werden auf gemeinsames Format normalisiert (Berlin TZ): -```python -{ - 'start': datetime, # Berlin TZ - 'end': datetime, - 'text': str, - 'notiz': str, - 'ort': str, - 'dauertermin': int, # 0/1 - 'turnus': int, # 0/1 - 'turnusArt': int, - 'recurrence': str # RRULE oder None -} -``` - -#### Advoware → Standard -- Start: `datum` + `uhrzeitVon` (Fallback 09:00), oder `datum` als datetime. -- End: `datumBis` + `uhrzeitBis` (Fallback 10:00), oder `datum` + 1h. -- All-Day: `dauertermin=1` oder Dauer >1 Tag. -- Recurring: `turnus`/`turnusArt` (vereinfacht, keine RRULE). - -#### Google → Standard -- Start/End: `dateTime` oder `date` (All-Day). -- All-Day: `dauertermin=1` wenn All-Day oder Dauer >1 Tag. -- Recurring: RRULE aus `recurrence`. - -#### Standard → Advoware -- POST/PUT: `datum`/`uhrzeitBis`/`datumBis` aus start/end. -- Defaults: `vorbereitungsDauer='00:00:00'`, `sb`/`anwalt`=employee_kuerzel. - -#### Standard → Google -- All-Day: `date` statt `dateTime`, end +1 Tag. -- Recurring: RRULE aus `recurrence`. - -## API-Schwächen und Fuckups - -### Advoware API (Buggy und Inkonsistent) -- **Case Sensitivity in Responses**: Feldnamen variieren – manchmal `'frNr'`, manchmal `'frnr'` (z.B. POST-Response: `{'frnr': 123}`). Code prüft beide (`result.get('frNr') or result.get('frnr')`), um None zu vermeiden. -- **Zeitformate**: `datum`/`datumBis` als `'YYYY-MM-DD'` oder `'YYYY-MM-DDTHH:MM:SS'`. `uhrzeitVon`/`uhrzeitBis` separat (z.B. `'09:00:00'`). Fehlt `uhrzeitVon`, Fallback 09:00; fehlt `uhrzeitBis`, 10:00. Parsing muss beide Formate handhaben. -- **Defaults und Fehlende Felder**: Viele Felder optional; Code setzt Fallbacks (z.B. `uhrzeitVon='09:00:00'`). -- **Recurring-Unterstützung**: Keine RRULE; nur `turnus` (0/1) und `turnusArt` (0-?). Mapping zu Google RRULE ist vereinfacht und unvollständig. -- **API-Zuverlässigkeit**: Manchmal erfolgreicher POST, aber `frNr: None` (trotz gültiger Response). 500-Fehler bei Bad Requests. Keine Timestamp-Details in Responses. -- **Zeitzonen**: Alles implizit Berlin; Code konvertiert explizit. -- **Andere Bugs**: `zuletztGeaendertAm` für Timestamps, aber Format unzuverlässig. - -### Google Calendar API (Zuverlässig) -- **Zeitformate**: `dateTime` als ISO mit TZ (z.B. `'2025-01-01T10:00:00+01:00'`), `date` für All-Day. Code parst mit `fromisoformat` und `.rstrip('Z')`. -- **Zeitzonen**: Explizit (z.B. `'Europe/Berlin'`); Code konvertiert zu Berlin TZ. -- **Recurring**: RRULE in `recurrence`; vollständig unterstützt. -- **Updates**: `updated` Timestamp für last-change. -- **Keine bekannten Bugs**: Zuverlässig, aber Rate-Limits möglich. - -## Step-Konfiguration - -### calendar_sync_event_step.py -- **Type:** event -- **Subscribes:** calendar.sync.triggered -- **Flows:** advoware - -**Event Data:** -```json -{ - "data": { - "body": { - "employee_kuerzel": "SB" // Optional, default "AI" - } - } -} -``` - -## Setup - -### PostgreSQL -1. PostgreSQL 17 installieren und starten (localhost-only). -2. Datenbank erstellen: `sudo -u postgres psql -f /tmp/create_db.sql` -3. User und Berechtigungen setzen. - -### Google API Credentials -1. Google Cloud Console Projekt erstellen. -2. Google Calendar API aktivieren. -3. Service Account erstellen. -4. `service-account.json` im Projekt bereitstellen. - -### Advoware API Credentials -OAuth-ähnliche Authentifizierung. - -### Umgebungsvariablen -```env -# PostgreSQL -POSTGRES_HOST=localhost -POSTGRES_USER=calendar_sync_user -POSTGRES_PASSWORD=your_password -POSTGRES_DB_NAME=calendar_sync_db - -# Google Calendar -GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH=service-account.json - -# Advoware API -ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/ -ADVOWARE_PRODUCT_ID=64 -ADVOWARE_APP_ID=your_app_id -ADVOWARE_API_KEY=your_api_key -ADVOWARE_KANZLEI=your_kanzlei -ADVOWARE_DATABASE=your_database -ADVOWARE_USER=your_user -ADVOWARE_ROLE=2 -ADVOWARE_PASSWORD=your_password -ADVOWARE_TOKEN_LIFETIME_MINUTES=55 -ADVOWARE_API_TIMEOUT_SECONDS=30 - -# Redis -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_DB_ADVOWARE_CACHE=1 -REDIS_TIMEOUT_SECONDS=5 -``` - -## Verwendung - -### Manueller Sync -```bash -curl -X POST "http://localhost:3000/events" \ - -H "Content-Type: application/json" \ - -d '{"type": "calendar.sync.triggered", "data": {"body": {"employee_kuerzel": "SB"}}}' -``` - -### Automatischer Sync -Cron-Step für regelmäßige Ausführung. - -## Fehlerbehandlung und Logging - -- **Transaktionen**: Pro Operation separat; Rollback nur für diese. -- **Logging**: Detailliert (Info/Debug für API, Warnung für Fehler). -- **API-Fehler**: Retry mit Backoff für Google; robust gegen Advoware-Bugs. -- **Datenfehler**: Fallbacks bei Parsing-Fehlern. - -## Sicherheit - -- DB-User mit minimalen Berechtigungen. -- Schreibberechtigung-Flags verhindern unbefugte Änderungen. -- Audit-Logs für Compliance. - -## Bekannte Probleme - -- Recurring-Events: Begrenzte Unterstützung; Advoware hat keine RRULE. -- Timestamps: Fehlende in Google können zu Fallback führen. -- Performance: Bei vielen Terminen könnte Paginierung helfen. - -## Erweiterungen - -- Inkrementelle Syncs basierend auf `last_sync`. -- Mehr Strategien (z.B. 'manual'). -- Webhooks für Echtzeit. -- Tests und Monitoring. - -### Datenbank-Schema -```sql --- Haupt-Tabelle -CREATE TABLE calendar_sync ( - sync_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - employee_kuerzel VARCHAR(10) NOT NULL, - advoware_frnr INTEGER, - google_event_id VARCHAR(255), - source_system source_system_enum NOT NULL, - sync_strategy sync_strategy_enum NOT NULL DEFAULT 'source_system_wins', - sync_status sync_status_enum NOT NULL DEFAULT 'synced', - advoware_write_allowed BOOLEAN NOT NULL DEFAULT FALSE, - deleted BOOLEAN NOT NULL DEFAULT FALSE, - last_sync TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Enums -CREATE TYPE source_system_enum AS ENUM ('advoware', 'google'); -CREATE TYPE sync_strategy_enum AS ENUM ('source_system_wins', 'last_change_wins'); -CREATE TYPE sync_status_enum AS ENUM ('pending', 'synced', 'failed'); - --- Audit-Tabelle -CREATE TABLE calendar_sync_audit ( - id SERIAL PRIMARY KEY, - sync_id UUID NOT NULL, - action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE - timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); - --- Indizes -CREATE UNIQUE INDEX idx_calendar_sync_advoware ON calendar_sync (employee_kuerzel, advoware_frnr) WHERE advoware_frnr IS NOT NULL; -CREATE UNIQUE INDEX idx_calendar_sync_google ON calendar_sync (employee_kuerzel, google_event_id) WHERE google_event_id IS NOT NULL; -``` - -## Funktionalität - -### Automatische Kalender-Erstellung -- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt. -- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB". -- Kalender wird mit dem Haupt-Google-Account (`lehmannundpartner@gmail.com`) als Owner geteilt. - -### Bidirektionale Synchronisation - -#### Fetch und Standardisierung -- Frische Daten werden von beiden APIs gefetcht (Zeitraum: 365 Tage zurück, 730 vor). -- Daten werden standardisiert: Start/End, Text, Notiz, Ort, Dauertermin, Turnus. -- Unterschiede werden basierend auf relevanten Feldern erkannt. - -#### Verarbeitung bestehender Termine -- Für jeden DB-Eintrag: Prüfe, ob Termin in beiden Systemen existiert. -- Bei Unterschieden: Löse Konflikt basierend auf `sync_strategy`. - - `source_system_wins`: Aktualisiere basierend auf `source_system`. - - `last_change_wins`: Vergleiche Timestamps (`zuletztGeaendertAm` in Advoware, `updated` in Google); bei Fehlen: Fallback auf `source_system_wins`. -- Schreibberechtigung: Nur aktualisiere Advoware, wenn `advoware_write_allowed = TRUE`. -- Bei Fehlen in einem System: Rekreiere oder propagiere Delete basierend auf Strategie. - -#### Neue Termine -- Neue aus Advoware: Erstelle in Google, füge DB-Eintrag hinzu (`source_system = 'advoware'`, `advoware_write_allowed = FALSE`). -- Neue aus Google: Erstelle in Advoware (falls erlaubt), füge DB-Eintrag hinzu (`source_system = 'google'`, `advoware_write_allowed = TRUE`). - -#### Löschungen -- Wenn Termin in beiden fehlt: Soft Delete in DB. -- Wenn in einem fehlt: Propagiere Delete basierend auf Strategie und Berechtigung. - -## API-Endpunkte - -### Advoware API -- `GET /api/v1/advonet/Termine?kuerzel={kuerzel}&from={date}&to={date}` - Termine eines Mitarbeiters -- `GET /api/v1/advonet/Termine?frnr={frnr}` - Einzelner Termin (für Timestamp) -- `POST /api/v1/advonet/Termine` - Neuen Termin erstellen -- `PUT /api/v1/advonet/Termine` - Termin aktualisieren -- `DELETE /api/v1/advonet/Termine?frnr={frnr}` - Termin löschen - -**API-Schwächen:** -- Zeitformate: `datum`/`datumBis` als `YYYY-MM-DD` oder `YYYY-MM-DDTHH:MM:SS`, `uhrzeitVon`/`uhrzeitBis` separat. -- Defaults: `uhrzeitVon` kann fehlen, dann 09:00; `uhrzeitBis` 10:00. -- Keine Recurring-Unterstützung (RRULE); nur `turnus`/`turnusArt` als Flags. - -### Google Calendar API -- Kalender-Management (erstellen, ACL setzen) -- Events: Listen, erstellen, aktualisieren, löschen -- Recurring: RRULE-Unterstützung -- Timestamps: `updated` für Änderungszeit - -## Step-Konfiguration - -### calendar_sync_event_step.py -- **Type:** event -- **Subscribes:** calendar.sync.triggered -- **Flows:** advoware - -**Event Data:** -```json -{ - "data": { - "body": { - "employee_kuerzel": "SB" // Optional, default "AI" für Test - } - } -} -``` - -## Setup - -### PostgreSQL -1. PostgreSQL 17 installieren und starten (localhost-only). -2. Datenbank erstellen: `sudo -u postgres psql -f /tmp/create_db.sql` -3. User und Berechtigungen setzen. - -### Google API Credentials -1. Google Cloud Console Projekt erstellen. -2. Google Calendar API aktivieren. -3. Service Account erstellen. -4. `service-account.json` im Projekt bereitstellen. - -### Advoware API Credentials -OAuth-ähnliche Authentifizierung. - -### Umgebungsvariablen -```env -# PostgreSQL -POSTGRES_HOST=localhost -POSTGRES_USER=calendar_sync_user -POSTGRES_PASSWORD=your_password -POSTGRES_DB_NAME=calendar_sync_db - -# Google Calendar -GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH=service-account.json - -# Advoware API (wie zuvor) -ADVOWARE_API_BASE_URL=https://www2.advo-net.net:90/ -ADVOWARE_PRODUCT_ID=64 -ADVOWARE_APP_ID=your_app_id -ADVOWARE_API_KEY=your_api_key -ADVOWARE_KANZLEI=your_kanzlei -ADVOWARE_DATABASE=your_database -ADVOWARE_USER=your_user -ADVOWARE_ROLE=2 -ADVOWARE_PASSWORD=your_password -ADVOWARE_TOKEN_LIFETIME_MINUTES=55 -ADVOWARE_API_TIMEOUT_SECONDS=30 - -# Redis (falls verwendet) -REDIS_HOST=localhost -REDIS_PORT=6379 -REDIS_DB_ADVOWARE_CACHE=1 -REDIS_TIMEOUT_SECONDS=5 -``` - -## Verwendung - -### Manueller Sync -```bash -curl -X POST "http://localhost:3000/events" \ - -H "Content-Type: application/json" \ - -d '{"type": "calendar.sync.triggered", "data": {"body": {"employee_kuerzel": "SB"}}}' -``` - -### Automatischer Sync -Cron-Step für regelmäßige Ausführung. - -## Datenmapping - -### Standardisierung -Beide Systeme werden auf gemeinsames Format normalisiert: -```python -{ - 'start': datetime, # Berlin TZ - 'end': datetime, - 'text': str, - 'notiz': str, - 'ort': str, - 'dauertermin': int, # 0/1 - 'turnus': int, # 0/1 - 'turnusArt': int, - 'recurrence': str # RRULE oder None -} -``` - -### Advoware → Google -- All-Day: Wenn `dauertermin=1` oder Dauer >1 Tag. -- Recurring: RRULE aus `turnus`/`turnusArt` (vereinfacht). - -### Google → Advoware -- Zeit: `datum`/`uhrzeitBis` aus Start/End. -- Recurring: `turnus=1` wenn RRULE vorhanden. - -## Fehlerbehandlung und Logging - -- **Transaktionen**: Rollback bei Fehlern. -- **Logging**: Detailliertes Info/Debug-Logging für jeden Schritt. -- **API-Fehler**: Retry mit Backoff für Google; detailliert für Advoware. -- **Datenfehler**: Fallbacks bei Parsing-Fehlern. - -## Sicherheit - -- DB-User mit minimalen Berechtigungen. -- Schreibberechtigung-Flags verhindern unbefugte Änderungen. -- Audit-Logs für Compliance. - -## Bekannte Probleme - -- Recurring-Events: Begrenzte Unterstützung; Advoware hat keine RRULE. -- Timestamps: Fehlende in Google können zu Fallback führen. -- Performance: Bei vielen Terminen könnte Paginierung helfen. - -## Erweiterungen - -- Inkrementelle Syncs basierend auf `last_sync`. -- Mehr Strategien (z.B. 'manual'). -- Webhooks für Echtzeit. -- Tests und Monitoring. \ No newline at end of file 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 4e2e4536..861f53d9 100644 --- a/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py +++ b/bitbylaw/steps/advoware_cal_sync/calendar_sync_event_step.py @@ -494,14 +494,19 @@ async def handler(event, context): async with conn.transaction(): await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id']) elif row['source_system'] == 'google' and row['advoware_write_allowed']: - # Propagate delete to Advoware + # Recreate in Advoware try: - await safe_delete_advoware_appointment(advoware, frnr, row['advoware_write_allowed']) - async with conn.transaction(): - await conn.execute("UPDATE calendar_sync SET deleted = TRUE, sync_status = 'synced' WHERE sync_id = $1;", row['sync_id']) - logger.info(f"Phase 3: Propagated delete to Advoware for sync_id {row['sync_id']}") + new_frnr = await safe_create_advoware_appointment(advoware, standardize_appointment_data(google_map[event_id], 'google'), employee_kuerzel, row['advoware_write_allowed']) + if new_frnr and str(new_frnr) != 'None': + async with conn.transaction(): + await conn.execute("UPDATE calendar_sync SET advoware_frnr = $1, sync_status = 'synced' WHERE sync_id = $2;", int(new_frnr), row['sync_id']) + logger.info(f"Phase 3: Recreated Advoware appointment {new_frnr} for sync_id {row['sync_id']}") + else: + logger.warning(f"Phase 3: Failed to recreate Advoware for sync_id {row['sync_id']}, frNr is None") + async with conn.transaction(): + await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id']) except Exception as e: - logger.warning(f"Phase 3: Failed to delete Advoware for sync_id {row['sync_id']}: {e}") + logger.warning(f"Phase 3: Failed to recreate Advoware for sync_id {row['sync_id']}: {e}") async with conn.transaction(): await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id']) else: