Fix timestamp-based update logic and remove global last_sync update to prevent redundant syncs and race conditions. Update README with anonymization and latest changes.

This commit is contained in:
root
2025-10-23 10:40:00 +00:00
parent 1de5bcd369
commit 9ab90fef5a
9 changed files with 910 additions and 323 deletions

View File

@@ -1,65 +1,155 @@
# Advoware Calendar Sync
# Advoware Calendar Sync - Hub-Based Design
Dieser Abschnitt implementiert die bidirektionale Synchronisation zwischen Advoware-Terminen und Google Calendar. Für jeden Mitarbeiter in Advoware wird automatisch ein entsprechender Google Calendar erstellt und gepflegt.
# Advoware Calendar Sync - Hub-Based Design
Dieser Abschnitt implementiert die bidirektionale Synchronisation zwischen Advoware-Terminen und Google Calendar unter Verwendung von PostgreSQL als zentralem Hub (Single Source of Truth). Das System stellt sicher, dass Termine konsistent gehalten werden, mit konfigurierbaren Konfliktauflösungsstrategien, Schreibberechtigungen und Datenschutzfeatures wie Anonymisierung. Der Sync läuft in vier strikten Phasen, um maximale Robustheit und Atomarität zu gewährleisten.
## Übersicht
Das System synchronisiert Termine zwischen:
- **Advoware**: Zentrale Terminverwaltung mit detaillierten Informationen
- **Google Calendar**: Benutzerfreundliche Kalenderansicht für jeden Mitarbeiter
- **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.
- **Timestamp-basierte Updates**: Updates werden ausschließlich auf Basis von `last_sync` (gesetzt auf den API-Timestamp der Quelle) getriggert, nicht auf Datenvergleichen, um Race-Conditions zu vermeiden.
- **Anonymisierung**: Optionale Anonymisierung sensibler Daten (Text, Notiz, Ort) bei Advoware → Google Sync, um Datenschutz zu wahren.
### 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 (API-Timestamp > `last_sync`).
### 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 (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
- 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 mit Token-Validierung
### Phasen-Details
#### Advoware → Google Calendar
- Alle Termine eines Mitarbeiters werden aus Advoware abgerufen (Zeitraum: aktuelles Jahr + 2 Jahre)
- Neue Termine werden in den entsprechenden Google Calendar eingetragen
- Die Advoware-Termin-ID (`frNr`) wird als Metadaten gespeichert
- Bestehende Termine werden aktualisiert, wenn Änderungen erkannt werden
#### Phase 1: Neue Einträge Advoware → Google
- Fetch Advoware-Termine.
- Für jede frNr, die nicht in DB (deleted=FALSE) existiert: Standardisiere Daten (mit Anonymisierung falls aktiviert), erstelle Google-Event, dann INSERT in DB mit `sync_status = 'synced'`, `last_sync` auf Advoware-Timestamp.
- Bei Fehlern: Warnung loggen, weitermachen (nicht abbrechen).
#### Google Calendar → Advoware
- Termine aus Google Calendar ohne `frNr` werden als neue Termine in Advoware erstellt
- Die generierte `frNr` wird zurück in den Google Calendar geschrieben
- Token-basierte Validierung verhindert unbefugte Änderungen
- Sync-Info wird in der Description/Notiz gespeichert
#### 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'`, `last_sync` auf Google-Timestamp.
- Bei frNr None (API-Bug): Skippen mit Warnung.
- Bei Fehlern: Warnung loggen, weitermachen.
### Token-Sicherheit
- MD5-Hash mit Salt (aus Umgebungsvariable `CALENDAR_SYNC_SALT`) für Änderungsvalidierung
- Sync-Info Format: `## no change below this line ##\nfrNr: {frNr}\nsync-token: {token}`
- Token wird bei jeder Änderung neu berechnet und validiert
#### 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.
### Löschungen
- Google-initiale Termine, die in Google gelöscht werden, werden auch in Advoware gelöscht
- Tracking von gelöschten `frNr` um Re-Sync zu verhindern
#### Phase 4: Bestehende Einträge updaten
- Für bestehende Einträge: Prüfe API-Timestamp > `last_sync`.
- Bei `source_system_wins`: Update basierend auf `source_system`, setze `last_sync` auf den API-Timestamp der Quelle.
- Bei `last_change_wins`: Vergleiche Timestamps, update das System mit dem neueren, setze `last_sync` auf den neueren Timestamp.
- Anonymisierung: Bei Advoware → Google wird Text/Notiz/Ort anonymisiert, wenn `CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS = True`.
- Bei Fehlern: `sync_status = 'failed'`, Warnung.
## API-Endpunkte
### 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 API
- `GET /api/v1/advonet/Mitarbeiter?aktiv=true` - Liste aktiver Mitarbeiter
- `GET /api/v1/advonet/Termine?frnr={frnr}` - Einzelner Termin
- `GET /api/v1/advonet/Termine?kuerzel={kuerzel}&from={date}&to={date}` - Termine eines Mitarbeiters
- `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
#### 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).
- Anonymisierung: Wenn `CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS`, setze `text='Advoware blocked'`, `notiz=''`, `ort=''`.
**API-Schwächen und Seltsamkeiten:**
- Parameter müssen als Strings übergeben werden (z.B. `aktiv='true'`, nicht `True`)
- Response kann manchmal HTML statt JSON zurückgeben, auch bei 200 Status
- Content-Type Header ist nicht immer korrekt gesetzt
- Token-Authentifizierung mit HMAC-Signature erforderlich
- Keine Paginierung für große Resultate
- Zeitformate: `datum` als `YYYY-MM-DDTHH:MM:SS`, aber manchmal ohne T
#### Google → Standard
- Start/End: `dateTime` oder `date` (All-Day).
- All-Day: `dauertermin=1` wenn All-Day oder Dauer >1 Tag.
- Recurring: RRULE aus `recurrence`.
### Google Calendar API
- Kalender-Management (erstellen, auflisten, ACL setzen)
- Event-Management (erstellen, aktualisieren, löschen)
- Service Account Authentifizierung mit Backoff bei Rate Limits
#### 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
@@ -71,41 +161,38 @@ Das System synchronisiert Termine zwischen:
**Event Data:**
```json
{
"full_content": true // oder false für nur "blocked"
"data": {
"body": {
"employee_kuerzel": "SB" // Optional, default "AI"
}
}
}
```
### calendar_sync_api_step.py
- **Type:** api
- **Path:** `/advoware/calendar/sync`
- **Method:** POST
- **Flows:** advoware
**Request Body:**
```json
{
"full_content": true // oder false für nur "blocked"
}
```
### calendar_sync_cron_step.py
- **Type:** cron
- **Schedule:** Täglich um 2:00 Uhr
- **Flows:** advoware
## 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 (kein OAuth)
4. `service-account.json` Datei im Projektverzeichnis bereitstellen
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 mit HMAC-Signature.
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
@@ -122,14 +209,14 @@ ADVOWARE_PASSWORD=your_password
ADVOWARE_TOKEN_LIFETIME_MINUTES=55
ADVOWARE_API_TIMEOUT_SECONDS=30
# Redis für Token Caching
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB_ADVOWARE_CACHE=1
REDIS_DB_CALENDAR_SYNC=1
REDIS_TIMEOUT_SECONDS=5
# Calendar Sync
CALENDAR_SYNC_SALT=your_secret_salt
# Anonymisierung
CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS=true # Optional, default false
```
## Verwendung
@@ -141,121 +228,490 @@ curl -X POST "http://localhost:3000/advoware/calendar/sync" \
-d '{"full_content": true}'
```
### Event-basierter Sync
```bash
curl -X POST "http://localhost:3000/events" \
-H "Content-Type: application/json" \
-d '{"type": "calendar.sync.triggered", "data": {"full_content": true}}'
### 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 und Datenschutz
- DB-User mit minimalen Berechtigungen.
- Schreibberechtigung-Flags verhindern unbefugte Änderungen.
- Anonymisierung: Verhindert Leakage sensibler Daten in Google Calendar.
- 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.
## Ü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;
```
### Automatischer Sync
Der Cron-Step führt täglich um 2:00 Uhr einen vollständigen Sync aus.
## Funktionalität
## Datenmapping
### 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.
### Advoware → Google Calendar
```javascript
### 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
{
"summary": "Advoware blocked", // Immer "blocked" für Privacy
"description": `Advoware Termin ID: ${appointment.frNr}`,
"location": "",
"start": {
"dateTime": start_datetime, // Berechnet aus datum/uhrzeitVon
"timeZone": "Europe/Berlin"
},
"end": {
"dateTime": end_datetime, // Berechnet aus datumBis/uhrzeitBis
"timeZone": "Europe/Berlin"
},
"extendedProperties": {
"private": {
"advoware_frnr": appointment.frNr.toString()
'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"
}
}
}
```
### Google Calendar → Advoware
```javascript
{
"frNr": int(frnr),
"text": event.summary,
"notiz": updated_description, // Mit sync-info
"ort": event.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"
}
## 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
```
## Fehlerbehandlung
## Verwendung
- **Google API Fehler:** Exponentieller Backoff bei 403/429, automatische Wiederholung
- **Advoware API Fehler:** Token-Refresh bei 401, detaillierte Logging
- **JSON Parse Fehler:** Response-Text logging für Debugging
- **Netzwerkfehler:** Timeout-Handling, konfigurierbare Timeouts
- **Dateninkonsistenzen:** Token-Validierung, Änderungsvergleich vor Update
### 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"}}}'
```
## Monitoring
### Automatischer Sync
Cron-Step für regelmäßige Ausführung.
### Logs
- Erfolgreiche Synchronisationen mit Anzahl
- Fehlerhafte API-Calls mit Details
- Kalender-Erstellungen und ACL-Setups
- Performance-Metriken (Sync-Dauer)
- Debug-Logs für Änderungsvergleiche
## Fehlerbehandlung und Logging
### Metriken
- Anzahl synchronisierter Termine
- Verarbeitete Mitarbeiter
- Fehlerquoten pro API
- Gelöschte Termine
- Token-Validierungsfehler
- **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
- Service Account für Google API (kein User-Consent)
- HMAC-SHA512 für Advoware Token-Generierung
- Token-Caching in Redis mit TTL
- MD5-Token für Sync-Validierung mit Salt
- Scoped Permissions (nur Calendar-Zugriff)
- Audit-Logs für alle Änderungen
- DB-User mit minimalen Berechtigungen.
- Schreibberechtigung-Flags verhindern unbefugte Änderungen.
- Audit-Logs für Compliance.
## Bekannte Probleme und Workarounds
## Bekannte Probleme
### Advoware API
- **Parameter-Typen:** Alle Query-Parameter müssen Strings sein (z.B. `'true'` statt `True`)
- **Response-Formate:** Manchmal HTML statt JSON, auch bei 200 Status
- **Content-Type:** Nicht immer korrekt gesetzt, führt zu Parse-Fehlern
- **Zeitformate:** Inkonsistente Formate, manuelle Parsing erforderlich
- **Rate Limits:** Keine dokumentierten Limits, aber praktische Limits vorhanden
### Google Calendar API
- **Rate Limits:** 403/429 mit Backoff-Handling
- **ACL-Sharing:** Owner-Rolle für Hauptaccount erforderlich
- **Event-Updates:** Vollständige Event-Daten bei Updates senden
### Sync-Logik
- **Token-Mismatch:** Verhindert unbefugte Änderungen
- **Deleted Tracking:** Verhindert Re-Sync gelöschter Termine
- **Änderungsvergleich:** Nur tatsächliche Änderungen syncen
- 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
### Geplante Features
- Inkrementelle Syncs (nur geänderte Termine seit letztem Sync)
- Konfliktlösungsstrategien (Advoware gewinnt, Google gewinnt, Manuell)
- Batch-Verarbeitung für bessere Performance
- Webhook-Integration für Echtzeit-Syncs
- Mehrere Google-Accounts unterstützen
- Outlook/Apple Calendar Integration
- Inkrementelle Syncs basierend auf `last_sync`.
- Mehr Strategien (z.B. 'manual').
- Webhooks für Echtzeit.
- Tests und Monitoring.
### Code-Verbesserungen
- Unit-Tests für API-Calls
- Mock-Server für Testing
- Config-Validation beim Startup
- Health-Checks für beide APIs
- Metrics-Export (Prometheus)
### 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.