Fix calendar sync Phase 3 logic for missing Advoware appointments when source is Google; update README with Advoware API peculiarities

This commit is contained in:
root
2025-10-23 11:13:15 +00:00
parent a58e6d10a6
commit 7d5403b4af
2 changed files with 16 additions and 468 deletions

View File

@@ -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. - **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. - **Zeitzonen**: Alles implizit Berlin; Code konvertiert explizit.
- **Andere Bugs**: `zuletztGeaendertAm` für Timestamps, aber Format unzuverlässig. - **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) ### 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')`. - **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 ## 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.

View File

@@ -494,14 +494,19 @@ async def handler(event, context):
async with conn.transaction(): async with conn.transaction():
await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id']) 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']: elif row['source_system'] == 'google' and row['advoware_write_allowed']:
# Propagate delete to Advoware # Recreate in Advoware
try: try:
await safe_delete_advoware_appointment(advoware, frnr, row['advoware_write_allowed']) new_frnr = await safe_create_advoware_appointment(advoware, standardize_appointment_data(google_map[event_id], 'google'), employee_kuerzel, row['advoware_write_allowed'])
async with conn.transaction(): if new_frnr and str(new_frnr) != 'None':
await conn.execute("UPDATE calendar_sync SET deleted = TRUE, sync_status = 'synced' WHERE sync_id = $1;", row['sync_id']) async with conn.transaction():
logger.info(f"Phase 3: Propagated delete to Advoware for sync_id {row['sync_id']}") 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: 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(): async with conn.transaction():
await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id']) await conn.execute("UPDATE calendar_sync SET sync_status = 'failed' WHERE sync_id = $1;", row['sync_id'])
else: else: