google configs
This commit is contained in:
parent
4c073c4f04
commit
97bef86276
6 changed files with 1047 additions and 10 deletions
|
|
@ -19,6 +19,72 @@ Diese Referenz fokussiert auf **FormGeneratorTable** und das zugehoerige Backend
|
|||
|
||||
---
|
||||
|
||||
## Page Layout Chain (Pflicht)
|
||||
|
||||
`FormGeneratorTable` rendert intern mit `flex:1; min-height:0; overflow:hidden`. Damit die Tabelle die volle Seitenbreite/-hoehe ausnuetzt und nicht abgeschnitten wird, **muss** die Eltern-Hierarchie eine **bounded height chain** liefern. Andernfalls passiert eines davon:
|
||||
|
||||
- Tabelle ist 0 px hoch (kollabiert) -- nur Toolbar sichtbar.
|
||||
- Tabelle waechst ueber den Viewport hinaus, horizontaler Scroll fehlt -- letzte Spalten werden abgeschnitten.
|
||||
- Page-Container hat `max-width` -> Tabelle ist auf z.B. 800 px begrenzt.
|
||||
|
||||
### Korrektes Pattern (verwendet PromptsPage, TrusteePositionsView, TrusteeDataTablesView)
|
||||
|
||||
```tsx
|
||||
import adminStyles from '../../admin/Admin.module.css';
|
||||
|
||||
export const MyPage: React.FC = () => (
|
||||
<div className={`${adminStyles.adminPage} ${adminStyles.adminPageFill}`}>
|
||||
<div className={adminStyles.pageHeader}>...</div>
|
||||
|
||||
<div className={adminStyles.tableContainer}>
|
||||
<FormGeneratorTable {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
Die drei CSS-Klassen aus `frontend_nyla/src/pages/admin/Admin.module.css`:
|
||||
|
||||
| Klasse | Wirkung |
|
||||
|--------|---------|
|
||||
| `.adminPage` | `display:flex; flex-direction:column; width:100%; box-sizing:border-box; flex:0 0 auto` (Default fuer Standard-Seiten ohne Tabelle) |
|
||||
| `.adminPageFill` | `flex:1 1 auto; min-height:0; overflow:hidden` -- aktiviert die bounded height chain |
|
||||
| `.tableContainer` | `flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column` -- direktes Eltern-Element der Tabelle |
|
||||
|
||||
`FeatureView`/`MainLayout` liefern bereits `height:100%` mit `min-height:0` -- Page-Komponenten brauchen also nur `adminPage adminPageFill` und einen `tableContainer` direkt um die `FormGeneratorTable`.
|
||||
|
||||
### Bei Tabs / Wrapper-Komponenten
|
||||
|
||||
Wenn die Tabelle in einem Tab-Wrapper (z.B. `TrusteeDataTab`) gemountet wird, muss **jeder** Wrapper die Flex-Chain weiterreichen. Pattern:
|
||||
|
||||
```tsx
|
||||
const _rootStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
width: '100%',
|
||||
};
|
||||
const _tableWrapStyle: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
};
|
||||
```
|
||||
|
||||
Eine Toolbar oberhalb der Tabelle bekommt `flexShrink: 0`, der Tabellenwrapper `flex: 1; minHeight: 0`.
|
||||
|
||||
### Anti-Patterns (verursachen das "Tabelle abgeschnitten"-Bug)
|
||||
|
||||
- Outer-Wrapper mit `max-width: 800px` (z.B. `.expenseImportSection` aus `TrusteeViews.module.css`) -- begrenzt die Breite.
|
||||
- Outer `<div>` ohne `flex:1; min-height:0` -- Tabelle kollabiert auf 0 px Hoehe.
|
||||
- `.adminPage` ohne `.adminPageFill` -- Default ist `flex:0 0 auto`, das innere `.tableContainer flex:1` hat keinen Platz.
|
||||
- Padding auf einem Zwischen-Wrapper -- bricht das Box-Sizing der Chain.
|
||||
|
||||
---
|
||||
|
||||
## FormGeneratorTable -- Props
|
||||
|
||||
### Datenanbindung
|
||||
|
|
|
|||
159
c-work/2-build/2026-04-trustee-data-tables-page.md
Normal file
159
c-work/2-build/2026-04-trustee-data-tables-page.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<!-- status: build -->
|
||||
<!-- started: 2026-04-20 -->
|
||||
<!-- component: gateway | frontend-nyla -->
|
||||
|
||||
# Trustee: Konsolidierte Daten-Tabellen-Seite
|
||||
|
||||
## Beschreibung und Kontext
|
||||
|
||||
Aktuell hat das Trustee-Feature zwei separate Top-Level-Seiten für Datensichten (`Positionen`, `Dokumente`). Die übrigen 11 Trustee-Tabellen (Stammdaten + Sync-Daten + Buchhaltungs-Config/Sync) sind im UI **nicht sichtbar** – nur als JSON-Export oder per AI-Agent erreichbar. Das ist intransparent: Anwender und Auditor:innen sehen die importierten Konten / Buchungen / Kontakte / Salden nirgends.
|
||||
|
||||
Lösung: Eine neue konsolidierte Seite `Daten-Tabellen` mit einem Tab pro Trustee-Tabelle. Jeder Tab nutzt `FormGeneratorTable` mit Pagination, Sortierung, Filterung und Suche – exakt das Pattern, das `PromptsPage` und die bestehenden `TrusteePositionsView` / `TrusteeDocumentsView` etabliert haben.
|
||||
|
||||
Business-Treiber:
|
||||
- Transparenz für Treuhänder:innen: «Was wurde wirklich aus Bexio/RMA importiert?»
|
||||
- Audit-Fähigkeit: Sync-Status und Sync-Snapshots direkt im UI prüfen.
|
||||
- Vereinheitlichung: Konsistente UI über alle Tabellen statt zwei isolierter Spezialseiten.
|
||||
|
||||
Risiko bei Verzicht: Anwender greifen via Export/Excel auf interne Daten zu (umständlich, fehleranfällig); Sync-Probleme bleiben unbemerkt.
|
||||
|
||||
## Fokus und kritische Details
|
||||
|
||||
- **FormGeneratorTable-Vertrag** (siehe `wiki/b-reference/frontend-nyla/formgenerator.md`): Jede Tabelle braucht (a) ein `apiEndpoint` mit Unified Filter API (`mode=filterValues|ids`), (b) einen Hook mit `refetch / pagination / attributes / permissions`, (c) saubere Spaltendefinitionen aus `getModelAttributeDefinitions(...)`.
|
||||
- **`getModelAttributeDefinitions` ist bereits da** (`/api/trustee/{instanceId}/attributes/{entityType}`) – aber nur registriert für die **6 CRUD-Modelle** (`_TRUSTEE_ENTITY_MODELS` in `routeFeatureTrustee.py`, Zeile 195). Die 7 fehlenden Modelle (TrusteeData* + Accounting*) müssen in dieser Map ergänzt werden.
|
||||
- **Sync-Tabellen sind read-only** (User-Entscheidung). Endpunkte für TrusteeData*/AccountingSync sind aktuell **nicht** als generische REST-CRUDs implementiert – nur Aggregat-Endpunkte (`/accounting/import-status`, `/accounting/sync-status`, `/accounting/export-data`). Es braucht **neue Read-Endpunkte** mit Pagination + Unified Filter API.
|
||||
- **RBAC-Konsistenz**: Jede Tabelle hat in `mainTrustee.DATA_OBJECTS` einen `data.feature.trustee.<Model>`-Key. Der Hook (`_createTrusteeEntityHook`) löst Permissions via `data.feature.trustee.{entityName}` auf – das funktioniert bereits für die bestehenden 6 Modelle, muss für die neuen 7 ebenfalls greifen.
|
||||
- **Backwards-Compat**: Die bestehenden Seiten `Positionen` und `Dokumente` bleiben **vorerst** erhalten (Navigation + Routes + RBAC). Erst wenn die neue Seite verifiziert ist, werden sie in einer Folge-Iteration entfernt.
|
||||
- **Tab-State via URL** (`?tab=positions`): Konsistent mit `TrusteeImportProcessView`, `TrusteeAccountingSettingsView`, `TrusteeAbschlussView`. Erlaubt Deep-Links (z.B. Quick Actions / Notification-Links) auf konkrete Tabellen.
|
||||
- **Performance**: Lazy-Mount pro Tab (Hook ruft nur Daten, wenn Tab aktiv ist) – sonst werden 13 Tabellen parallel geladen.
|
||||
- **i18n-Pflicht**: Alle Tab-Labels via `t(...)`. Spalten-Labels kommen aus dem Backend-`@i18nModel`.
|
||||
- **Hidden System-Felder**: `mandateId`, `featureInstanceId`, technische `id`-UUIDs sollen tabellenweit hidden sein (gleiche Liste wie in PromptsPage `hiddenColumns`).
|
||||
- **Naming**: Funktionen `_`-Prefix für intern (`_buildEntityHookConfig`, `_renderTabBar`); camelCase überall.
|
||||
|
||||
## Ziel und Nicht-Ziele
|
||||
|
||||
- **Ziel**: Neue Seite `Daten-Tabellen` (URL: `/mandates/{mandateId}/trustee/{instanceId}/data-tables[?tab=<key>]`) mit einem Tab pro Trustee-Tabelle. Jeder Tab nutzt `FormGeneratorTable` mit Pagination/Sort/Filter/Search/Inline-Edit (für CRUD-Tabellen) bzw. read-only (für Sync-Tabellen).
|
||||
- **Ziel**: Backend liefert Attribute & paginierte Daten via Unified Filter API auch für die 7 bisher UI-losen Tabellen.
|
||||
- **Ziel**: Tab-Sichtbarkeit folgt RBAC (`data.feature.trustee.<Model>` view-Permission).
|
||||
- **Ziel**: Konsistentes UX-Pattern mit `PromptsPage` (Header / Refresh / FormGeneratorTable / optional Edit-Modal via FormGeneratorForm für CRUD-Tabellen).
|
||||
- **Explizit NICHT**: Entfernen der alten `Positionen`/`Dokumente`-Seiten in dieser Iteration (Folge-Plan). Routen + Quick Actions + RBAC bleiben unangetastet.
|
||||
- **Explizit NICHT**: Inline-Edit oder Bulk-Sync für TrusteeData*-Tabellen – sie sind reine Sync-Spiegel.
|
||||
- **Explizit NICHT**: Neuer DB-Schema-Wandel oder Migration – nur Read-Endpunkte ergänzen.
|
||||
- **Explizit NICHT**: Beleg-Download-Spalte oder Sync-Status-Spalte im Positions-Tab – die Spezialdarstellung bleibt der eigenständigen `TrusteePositionsView` vorbehalten (Generic-Tab zeigt das «rohe» Modell).
|
||||
|
||||
## Betroffene Module
|
||||
|
||||
- **Gateway**:
|
||||
- `gateway/modules/features/trustee/routeFeatureTrustee.py`: `_TRUSTEE_ENTITY_MODELS` um 7 Modelle erweitern; neue Read-Endpunkte mit Pagination + Unified Filter API für `TrusteeDataAccount`, `TrusteeDataJournalEntry`, `TrusteeDataJournalLine`, `TrusteeDataContact`, `TrusteeDataAccountBalance`, `TrusteeAccountingConfig`, `TrusteeAccountingSync`.
|
||||
- `gateway/modules/features/trustee/mainTrustee.py`: `UI_OBJECTS` um `ui.feature.trustee.data-tables` ergänzen; `TEMPLATE_ROLES.accessRules` der relevanten Rollen erweitern.
|
||||
- **Frontend**:
|
||||
- **Neu**: `frontend_nyla/src/pages/views/trustee/TrusteeDataTablesView.tsx` (Container mit Tab-Bar + URL-Sync).
|
||||
- **Neu**: `frontend_nyla/src/pages/views/trustee/dataTables/TrusteeDataTab.tsx` (generischer Tab-Body mit FormGeneratorTable für CRUD- und Read-only-Modi).
|
||||
- **Erweitern**: `frontend_nyla/src/hooks/useTrustee.ts` um Hooks für die 7 neuen Modelle (über die bestehenden Factories `_createTrusteeEntityHook` / `_createTrusteeOperationsHook` – read-only Variante für Sync-Tabellen).
|
||||
- **Erweitern**: `frontend_nyla/src/api/trusteeApi.ts` um `fetchTrusteeData*`-Funktionen.
|
||||
- **Anpassen**: `frontend_nyla/src/pages/FeatureView.tsx` (`VIEW_COMPONENTS.trustee` um `'data-tables': TrusteeDataTablesView` ergänzen).
|
||||
- **Anpassen**: `frontend_nyla/src/pages/views/trustee/index.ts` (Export).
|
||||
- **Anpassen**: `frontend_nyla/src/App.tsx` (Route `data-tables` registrieren).
|
||||
- **Optional**: `frontend_nyla/src/types/mandate.ts` (`FEATURE_REGISTRY.trustee.views`) um `data-tables` ergänzen, falls noch genutzt.
|
||||
- **DB-Migration**: nein.
|
||||
- **Andere Komponenten**: keine.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
|-------|-------------|------------|
|
||||
| 2026-04-20 | Seitenname: `Daten-Tabellen` | User-Entscheidung; klar, präzise, dt. |
|
||||
| 2026-04-20 | Alte Seiten `Positionen` + `Dokumente` bleiben vorerst | User-Entscheidung; Sicherheitsnetz bis neue Seite verifiziert ist – Aufräumen in Folge-Plan. |
|
||||
| 2026-04-20 | Sync-Tabellen (`TrusteeData*`, `TrusteeAccountingSync`, `TrusteeAccountingConfig`) sind read-only im UI | User-Entscheidung; Daten kommen aus externem System – Edits würden bei nächstem Sync überschrieben. |
|
||||
| 2026-04-20 | Alle 13 Tabellen erhalten einen Tab (User wählte "alle" – im Datenmodell sind es 13: 4 Stammdaten + 2 CRUD-Daten + 5 Sync-Daten + 2 Accounting). | User-Entscheidung; vollständige Transparenz. |
|
||||
| 2026-04-20 | Tab-State via URL-Param `?tab=<key>` | Konsistent mit allen anderen Trustee-Tab-Seiten. |
|
||||
| 2026-04-20 | Lazy-Mount pro Tab (kein Pre-Fetch der inaktiven Tabs) | Performance: 13 paginierte Endpunkte parallel laden ist unnötig. |
|
||||
| 2026-04-20 | Generische Tab-Komponente (`TrusteeDataTab`) statt 13 spezialisierter Views | DRY; Spezialisierungen (Beleg-Download/Sync-Status-Spalte) bleiben ausschliesslich in `TrusteePositionsView` / `TrusteeDocumentsView`. |
|
||||
| 2026-04-20 | Keine Bulk-Aktionen im neuen Tab (kein Sync-Button etc.) | Bulk-Sync ist im Positions-Tab schon vorhanden – Generic-Seite bleibt schlank. |
|
||||
|
||||
## Umsetzungs-Checkliste
|
||||
|
||||
### Backend (Gateway)
|
||||
|
||||
- [x] `_TRUSTEE_ENTITY_MODELS` in `routeFeatureTrustee.py` um die 7 neuen Modelle erweitert.
|
||||
- [x] Neue paginierte Read-Endpunkte (analog `get_documents`/`get_positions` mit `_handleDocumentMode`-Pattern) für jede der 7 Tabellen:
|
||||
- `GET /api/trustee/{instanceId}/data/accounts`
|
||||
- `GET /api/trustee/{instanceId}/data/journal-entries`
|
||||
- `GET /api/trustee/{instanceId}/data/journal-lines`
|
||||
- `GET /api/trustee/{instanceId}/data/contacts`
|
||||
- `GET /api/trustee/{instanceId}/data/account-balances`
|
||||
- `GET /api/trustee/{instanceId}/accounting/configs`
|
||||
- `GET /api/trustee/{instanceId}/accounting/syncs`
|
||||
- [x] Helper extrahiert: `_paginatedReadEndpoint(...)` haelt Logik fuer alle 7 Endpunkte zentral.
|
||||
- [x] RBAC: jeder neue Endpunkt prüft via `_validateInstanceAccess`; Datenzugriff wird auf SQL-Ebene durch `getRecordsetPaginatedWithRBAC` (DATA-Context, `data.feature.trustee.<Model>`) gefiltert – identisch zum bestehenden Documents/Positions-Pattern.
|
||||
- [x] `mainTrustee.py`: Neuer UI-Object-Eintrag `ui.feature.trustee.data-tables` (Label «Daten-Tabellen»).
|
||||
- [x] `mainTrustee.py`: AccessRule fuer `ui.feature.trustee.data-tables` bei `trustee-viewer`, `trustee-user`, `trustee-accountant` ergaenzt; Admin hat Wildcard.
|
||||
|
||||
### Frontend
|
||||
|
||||
- [x] `frontend_nyla/src/api/trusteeApi.ts`: 7 neue Read-Funktionen (`fetchDataAccounts`, `fetchDataJournalEntries`, `fetchDataJournalLines`, `fetchDataContacts`, `fetchDataAccountBalances`, `fetchAccountingConfigs`, `fetchAccountingSyncs`) plus passende Typen.
|
||||
- [x] `frontend_nyla/src/hooks/useTrustee.ts`: 7 neue Read-only-Hooks via `_createTrusteeEntityHook` (`_buildReadOnlyConfig` injiziert no-op-Mutatoren).
|
||||
- [x] **Neu** `frontend_nyla/src/pages/views/trustee/dataTables/TrusteeDataTab.tsx` – generischer Tab-Body, bindet `FormGeneratorTable` ein, respektiert `readOnly`.
|
||||
- [x] **Neu** `frontend_nyla/src/pages/views/trustee/TrusteeDataTablesView.tsx`:
|
||||
- 13 Tab-Defs, jede mit eigenem Wrapper-Component, das den passenden Hook aufruft.
|
||||
- URL-Sync via `?tab=<key>`, Lazy-Mount (nur aktiver Wrapper rendert -> kein Datenfetch fuer inaktive Tabs).
|
||||
- Tab-Bar im Stil von `TrusteeAbschlussView`/`TrusteeImportProcessView`; Sync-Tabs zeigen «(read-only)»-Hinweis.
|
||||
- RBAC-Filter im Frontend deaktiviert: einzelne Tab-Hooks blockieren Datenzugriff serverseitig (vermeidet sync-Permission-Lookups; spaetere Iteration kann Tab-Hiding nachziehen).
|
||||
- [x] `frontend_nyla/src/pages/views/trustee/index.ts`: Export ergaenzt.
|
||||
- [x] `frontend_nyla/src/pages/FeatureView.tsx`: Mapping `'data-tables': TrusteeDataTablesView` + Import.
|
||||
- [x] `frontend_nyla/src/App.tsx`: Route `<Route path="data-tables" .../>` ergaenzt.
|
||||
- [x] `frontend_nyla/src/types/mandate.ts`: `FEATURE_REGISTRY.trustee.views` um `'data-tables'` ergaenzt.
|
||||
|
||||
### Cross-Cutting
|
||||
|
||||
- [ ] RBAC / Permissions: alle neuen Endpunkte enforced via bestehendes `_validateInstanceAccess`; UI-Sichtbarkeit per `data.feature.trustee.<Model>`-Check (Hook liefert das via `permissions.view`).
|
||||
- [ ] Neutralisierung betroffen? **Nein** – keine AI-Calls, kein neuer Datenfluss zu externen Systemen.
|
||||
- [ ] Navigation / Routing: neue UI-Route `/mandates/{m}/trustee/{i}/data-tables[?tab=<key>]`.
|
||||
- [ ] Billing-Impact? **Nein**.
|
||||
- [ ] i18n: alle Tab-Labels und Header-Strings via `t(...)`; Spalten-Labels kommen aus `@i18nModel`-getaggten Modellen (bereits vorhanden für alle 13).
|
||||
- [ ] Quick Actions: keine Änderung – Quick Actions zeigen weiterhin auf `import-process` / `settings`. Optional in Folge-Iteration: Direktlinks auf einzelne Datentabs.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium (Given-When-Then) | Prio |
|
||||
|---|----------------------------|------|
|
||||
| 1 | Given ein Trustee-Admin auf einer Trustee-Instanz, When er auf Navigation «Daten-Tabellen» klickt, Then erscheint eine Seite mit Tab-Bar und allen 13 Trustee-Tabellen als Tabs. | must |
|
||||
| 2 | Given die Seite «Daten-Tabellen» mit Default-Tab, When der User sie öffnet, Then wird sofort der erste Tab geladen (FormGeneratorTable mit Daten, Pagination, Spalten aus `attributes`-API). | must |
|
||||
| 3 | Given ein Tab mit Sync-Daten (z.B. «Buchungen (Sync)»), When der User die Zeile inspiziert, Then sind keine Edit-/Delete-Buttons sichtbar (read-only). | must |
|
||||
| 4 | Given ein Tab mit CRUD-Daten (z.B. «Position»), When der User auf Edit klickt, Then erscheint ein FormGeneratorForm-Modal und Speichern persistiert via PUT-Endpunkt. | must |
|
||||
| 5 | Given die Seite «Daten-Tabellen», When der User auf Tab «Kontakte (Sync)» wechselt, Then ändert sich die URL auf `?tab=contacts` und nur der Kontakte-Tab ist gemountet (Network-Tab zeigt nur den Kontakte-API-Call). | must |
|
||||
| 6 | Given ein Tab mit aktiviertem `apiEndpoint`, When der User in einer Spalte einen Filter setzt, Then werden Distinct-Werte vom Backend via `?mode=filterValues&column=<x>` geladen. | must |
|
||||
| 7 | Given ein User OHNE Permission `data.feature.trustee.TrusteeDataContact` (view), When er die Seite öffnet, Then ist der Tab «Kontakte (Sync)» nicht sichtbar oder disabled. | should |
|
||||
| 8 | Given die Seite mit Deep-Link `?tab=accounts`, When der User per URL einsteigt, Then wird der Konten-Tab direkt aktiv geladen. | should |
|
||||
| 9 | Given die alten Seiten «Positionen»/«Dokumente», When der User sie aufruft, Then funktionieren sie unverändert weiter (kein Regress). | must |
|
||||
| 10 | Given ein Tab mit grosser Datenmenge (>1'000 Einträge), When der User scrollt, Then wird Server-Pagination angewandt (page-Size 25, kein Frontend-Speicher-Overload). | must |
|
||||
| 11 | Given Tabellen-Sortierung, When der User auf einen Spalten-Header klickt, Then wird per `pagination.sort` serverseitig sortiert. | must |
|
||||
|
||||
## Testplan
|
||||
|
||||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||||
|----|----|-----|--------------|-----------|--------|
|
||||
| T1 | 1 | manual UI | nein | – | pending |
|
||||
| T2 | 2,5 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py | pending |
|
||||
| T3 | 3,4 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py | pending |
|
||||
| T4 | 6 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (Unified Filter API: mode=filterValues / mode=ids) | pending |
|
||||
| T5 | 7 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (RBAC-Gate) | pending |
|
||||
| T6 | 8 | manual UI | nein | – | pending |
|
||||
| T7 | 9 | manual + e2e | teils | bestehende Position/Document Tests laufen weiter | pending |
|
||||
| T8 | 10,11 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (Pagination/Sort) | pending |
|
||||
|
||||
## Links
|
||||
|
||||
- PR: –
|
||||
- Issue: –
|
||||
- Vorlage: `wiki/b-reference/frontend-nyla/formgenerator.md`
|
||||
- Vorbild-Seite: `frontend_nyla/src/pages/basedata/PromptsPage.tsx`
|
||||
- Vorbild-Tabs: `frontend_nyla/src/pages/views/trustee/TrusteeAbschlussView.tsx`, `TrusteeAccountingSettingsView.tsx`
|
||||
|
||||
## Abschluss
|
||||
|
||||
- [ ] `b-reference/gateway/features/trustee.md` (oder analog) aktualisieren: neue Endpunkte + neue UI-Seite dokumentieren.
|
||||
- [ ] `b-reference/frontend-nyla/architecture.md`: kurze Erwähnung der konsolidierten `TrusteeDataTablesView` + Tab-Pattern für mehrere Modelle.
|
||||
- [ ] `TOPICS.md`: keine Änderung nötig (kein neues Thema).
|
||||
- [ ] Folge-Plan `2026-MM-trustee-cleanup-positions-documents.md`: Aufräumen der alten Seiten, sobald Daten-Tabellen produktiv verifiziert.
|
||||
- [ ] Dieses Dokument → `c-work/2-build/` (bei Umsetzungsbeginn) → `c-work/3-validate/` → `z-archive/`.
|
||||
267
c-work/4-done/2026-04-period-picker-billing-audit-migration.md
Normal file
267
c-work/4-done/2026-04-period-picker-billing-audit-migration.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
<!-- status: done -->
|
||||
<!-- started: 2026-04-20 -->
|
||||
<!-- finished: 2026-04-20 -->
|
||||
<!-- component: gateway, frontend-nyla -->
|
||||
|
||||
# Statistik-Endpunkte: Clean-Cut auf `dateFrom`/`dateTo` + `bucketSize`
|
||||
|
||||
## Beschreibung und Kontext
|
||||
|
||||
Folge-Arbeit zum [`PeriodPicker`](../../local/notes/changelog.txt). Die
|
||||
Komponente liefert semantisch sauber `{ preset, fromDate, toDate }`. Drei
|
||||
Statistik-Endpunkte im Gateway erwarten heute aber Legacy-Parameter, die
|
||||
Date-Range mit anderen Konzepten vermischen:
|
||||
|
||||
| Endpunkt | Heute | Problem |
|
||||
|---|---|---|
|
||||
| `GET /api/audit/stats` | `?timeRange={days}` (relative Tagesanzahl ab heute) | Kein echter Bereich; im Frontend muss `PeriodValue` per Adapter `_periodToDays` auf eine Tagesanzahl reduziert werden, dadurch wird "01.04 - 03.04" als "letzte 3 Tage ab heute" ausgewertet → falsche KPIs. |
|
||||
| `GET /api/billing/statistics/{period}` | `period: 'day' \| 'month' \| 'year'`, `year`, `month?` | `period` mischt zwei Verantwortungen: Date-Range-Berechnung **und** Bucket-Granularitaet; aerbitraere Ranges (z.B. "letzte 12 Monate", Custom) sind nicht ausdrueckbar. |
|
||||
| `GET /api/billing/view/statistics` | `period: 'day' \| 'month'`, `year`, `month?` | Dito; zusaetzlich gibt `period` an `getTransactionStatisticsAggregated(..., period=)` die Bucket-Groesse weiter. |
|
||||
|
||||
Konsequenz heute: der PeriodPicker-Rollout in `BillingDashboard` und
|
||||
`BillingDataView` ist blockiert; in `ComplianceAuditPage` arbeiten wir mit
|
||||
einem dokumentierten Hack (`_periodToDays`).
|
||||
|
||||
**Diese Arbeit ist ein Clean-Cut**: die Legacy-Parameter werden entfernt, die
|
||||
Endpunkte akzeptieren ausschliesslich `dateFrom`/`dateTo` (+ `bucketSize`,
|
||||
wo Time-Series ausgegeben werden), und alle Frontend-Caller werden in derselben
|
||||
Aenderung migriert. Keine Backwards-Compat-Aliase, kein "altes UND neues",
|
||||
kein toleranter Fallback im Backend.
|
||||
|
||||
## Fokus und kritische Details
|
||||
|
||||
- **Eine Aenderung = eine PR**: Backend-Refactor + alle Frontend-Caller
|
||||
zusammen, da ohne Aliase ein gemischter Zustand nicht laeuft.
|
||||
- **`/api/billing/view/statistics` mischt zwei Konzepte**: (a) Date-Range
|
||||
(`startTs/endTs`) und (b) Bucket-Granularitaet fuer Time-Series
|
||||
(`getTransactionStatisticsAggregated(..., period=)` -> `day` vs `month`).
|
||||
Diese **muessen getrennt werden**: Date-Range = `dateFrom`/`dateTo`,
|
||||
Bucket-Groesse = neuer expliziter Param `bucketSize: 'day' | 'month' | 'year'`.
|
||||
Default-Heuristik nur im Frontend, nicht im Backend.
|
||||
- **Timezones**: alle Audit-Logger-Records sind UTC-Epoch-Sekunden; PeriodPicker
|
||||
liefert lokale ISO-Daten (`YYYY-MM-DD`) ohne Timezone. Konversion eindeutig
|
||||
und an genau einer Stelle (im Endpunkt selbst, vor der DB-Abfrage):
|
||||
`dateFrom` -> `00:00:00 UTC` desselben Tages, `dateTo` -> `23:59:59.999 UTC`
|
||||
desselben Tages (inklusive). Sonst fehlt der letzte Tag in der Aggregation.
|
||||
→ Ein gemeinsamer Helper `_isoDateRangeToUtcEpoch(dateFrom, dateTo)` in
|
||||
`gateway/modules/shared/dateRange.py`.
|
||||
- **Validierung im Backend**: `dateFrom <= dateTo`, beide Pflicht (kein
|
||||
optional/None), `bucketSize` aus geschlossenem Enum. Bei Verletzung HTTP 400
|
||||
mit klarer Message. Keine impliziten Defaults.
|
||||
- **Aufrufende Stellen vollstaendig erfassen** (geprueft per ripgrep, siehe
|
||||
unten - keine externen Konsumenten ausserhalb des Frontends).
|
||||
|
||||
## Ziel und Nicht-Ziele
|
||||
|
||||
- **Ziel:**
|
||||
- `/api/audit/stats` akzeptiert ausschliesslich `dateFrom`/`dateTo` (ISO
|
||||
`YYYY-MM-DD`, Pflicht). Antwort enthaelt `dateFrom`/`dateTo`/`days` statt
|
||||
`timeRangeDays`.
|
||||
- `/api/billing/statistics` (Endpunkt-Pfad-Param `{period}` entfaellt, neuer
|
||||
Pfad `GET /api/billing/statistics`) akzeptiert ausschliesslich
|
||||
`dateFrom`/`dateTo` (Pflicht) + optional `bucketSize`.
|
||||
- `/api/billing/view/statistics` analog.
|
||||
- `aiAuditLogger.getAiAuditStats(mandateId, *, fromTs, toTs, groupBy)` -
|
||||
Pflicht-Range.
|
||||
- `getTransactionStatisticsAggregated(..., bucketSize=)` - Param `period`
|
||||
umbenannt zu `bucketSize`.
|
||||
- Frontend: `BillingDashboard`, `BillingDataView`, `ComplianceAuditPage`
|
||||
nutzen `<PeriodPicker>` und schicken `dateFrom`/`dateTo`. Adapter
|
||||
`_periodToDays` ersatzlos entfernt. `useBilling.loadStatistics`-Signatur
|
||||
nur noch `({ dateFrom, dateTo, bucketSize? })`.
|
||||
- **Explizit NICHT:**
|
||||
- Aliase `timeRange` / `period` / `year` / `month` im Backend behalten.
|
||||
- Stripe-`current_period_*` umbenennen oder beruehren - Stripe-API.
|
||||
- Andere Stat-Endpunkte (`/api/admin/database-health/stats`, etc.) anfassen -
|
||||
eigener Scope, sobald Bedarf.
|
||||
- DB-Schema aendern.
|
||||
|
||||
## Betroffene Module
|
||||
|
||||
- **Gateway:**
|
||||
- `gateway/modules/routes/routeAudit.py` (`getAuditStats`, Z. 303-317)
|
||||
- `gateway/modules/shared/aiAuditLogger.py` (`getAiAuditStats`, Z. 195-249)
|
||||
- `gateway/modules/routes/routeBilling.py`
|
||||
(`getStatistics` Z. 526-604, `getUserViewStatistics` Z. 1619 ff.,
|
||||
`UsageReportResponse` Z. 260)
|
||||
- `gateway/modules/interfaces/interfaceBilling.py` (oder konkrete Impl):
|
||||
`calculateStatisticsFromTransactions` (akzeptiert bereits `startDate`/`endDate`
|
||||
→ keine Aenderung), `getTransactionStatisticsAggregated`
|
||||
(`period` → `bucketSize`).
|
||||
- **Neu**: `gateway/modules/shared/dateRange.py` - kleiner Helper
|
||||
`parseIsoDateRange(dateFrom, dateTo) -> (date, date)` mit Validierung,
|
||||
`isoDateRangeToUtcEpoch(dateFrom, dateTo) -> (float, float)`.
|
||||
- **Frontend:**
|
||||
- `frontend_nyla/src/api/billingApi.ts` (`fetchStatistics`,
|
||||
`fetchViewStatistics`)
|
||||
- `frontend_nyla/src/hooks/useBilling.ts` (`loadStatistics` Signatur
|
||||
aendern)
|
||||
- `frontend_nyla/src/pages/billing/BillingDashboard.tsx` (Selects raus,
|
||||
PeriodPicker rein, optional separater `bucketSize`-Toggle)
|
||||
- `frontend_nyla/src/pages/billing/BillingDataView.tsx`
|
||||
(FormGeneratorReport: `periodSelector` raus, `dateRangeSelector` rein)
|
||||
- `frontend_nyla/src/pages/ComplianceAuditPage.tsx`: `_periodToDays`
|
||||
entfernen, `_loadStats({ dateFrom, dateTo })`.
|
||||
- `frontend_nyla/src/api/auditApi.ts` (oder direkt in der Page, je nach
|
||||
aktueller Struktur).
|
||||
- **DB-Migration:** nein.
|
||||
- **Tests:** Vorhandene Tests auf den 3 Endpunkten mitziehen (siehe
|
||||
Aufwandsschaetzung).
|
||||
|
||||
## Externe Konsumenten - Pruefung
|
||||
|
||||
Vor Clean-Cut explizit verifizieren, dass keine externen Aufrufer
|
||||
(Webhooks, Skripte, Excel-Reports, Teams-Bot, private-llm) die Endpunkte
|
||||
direkt nutzen:
|
||||
|
||||
- [ ] `rg "billing/statistics" gateway/ private-llm/ teams-bot/`
|
||||
- [ ] `rg "audit/stats" gateway/ private-llm/ teams-bot/`
|
||||
- [ ] `rg "/api/billing/statistics|/api/audit/stats" frontend_nyla/`
|
||||
|
||||
Wenn Treffer ausserhalb des Frontends auftauchen, werden sie im selben PR
|
||||
mitgezogen. Wenn nicht, kann der Clean-Cut erfolgen.
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Datum | Entscheidung | Begründung |
|
||||
|-------|-------------|------------|
|
||||
| 2026-04-20 | Clean-Cut, keine Backwards-Compat-Aliase | Vermischte Param-Konzepte sind die Wurzel der UX-Probleme; sie zu konservieren reproduziert genau das Problem, das wir loesen. Es gibt keine externen Konsumenten ausserhalb unseres Frontends (zu verifizieren, siehe oben). |
|
||||
| 2026-04-20 | `dateFrom`/`dateTo` als ISO `YYYY-MM-DD` String, **nicht** Epoch | Konsistent mit `PeriodValue`, lesbar in Logs/Browsernetzwerk, eindeutig (Tagesgrenze, kein Sub-Tag). Audit-Logger arbeitet zwar intern mit Epoch, aber die Konversion macht der Endpunkt zentral. |
|
||||
| 2026-04-20 | `dateFrom`/`dateTo` Pflicht, kein impliziter Default | Verhindert "vergessene Filter" (z.B. globale Aggregation ueber alle Zeit, schlecht fuer Performance). Caller muss bewusst einen Bereich waehlen. Defaults gehoeren ins Frontend. |
|
||||
| 2026-04-20 | `bucketSize` separater Param, nicht in `dateFrom`/`dateTo` impliziert | Eine Verantwortung pro Param. Erlaubt "letzte 30 Tage in Tagen" UND "letzte 30 Tage in Wochen" ohne Backend-Aenderung. |
|
||||
| 2026-04-20 | `bucketSize` ohne Default | Fuer Endpunkte mit Time-Series Antwort: Caller muss explizit waehlen, sonst HTTP 400. Frontend kapselt sinnvolle Heuristik. |
|
||||
| 2026-04-20 | Pfad `GET /api/billing/statistics` ohne `{period}` | `period` war Pfad-Param, mit Wegfall hat es im Pfad nichts mehr verloren. Saubere Resource-URL. |
|
||||
|
||||
## Umsetzungs-Checkliste
|
||||
|
||||
### A. Vorbereitung (~30 min)
|
||||
|
||||
- [ ] Konsumenten-Pruefung wie oben (drei `rg`-Aufrufe).
|
||||
- [ ] `gateway/modules/shared/dateRange.py` anlegen mit
|
||||
`parseIsoDateRange(dateFrom: str, dateTo: str) -> tuple[date, date]`
|
||||
(HTTP 400 bei ungueltig oder `from > to`) und
|
||||
`isoDateRangeToUtcEpoch(dateFrom: str, dateTo: str) -> tuple[float, float]`.
|
||||
Unit-Tests dazu.
|
||||
|
||||
### B. Backend Audit-Stats (~1 h)
|
||||
|
||||
- [ ] `aiAuditLogger.getAiAuditStats(mandateId, *, fromTs: float, toTs: float, groupBy: str = "model")`:
|
||||
Cutoff-Berechnung raus, ersetzt durch Range-Filter `fromTs <= ts <= toTs`.
|
||||
`timeRangeDays`-Feld in der Antwort durch `dateFrom`, `dateTo`, `days`
|
||||
(`(toTs - fromTs) / 86400`, gerundet) ersetzen.
|
||||
- [ ] `routeAudit.getAuditStats`:
|
||||
```python
|
||||
dateFrom: str = Query(..., description="ISO YYYY-MM-DD")
|
||||
dateTo: str = Query(..., description="ISO YYYY-MM-DD")
|
||||
groupBy: str = Query("model", regex="^(model|user|feature|day)$")
|
||||
```
|
||||
→ ueber `isoDateRangeToUtcEpoch` an Logger weitergeben. `timeRange`
|
||||
Param weg.
|
||||
|
||||
### C. Backend Billing-Stats (~2 h)
|
||||
|
||||
- [ ] `getTransactionStatisticsAggregated`: Param `period` umbenennen zu
|
||||
`bucketSize` (Enum-Validierung im Endpunkt, hier nur Type-Hint Literal).
|
||||
Alle Aufrufe im Modul mitziehen.
|
||||
- [ ] `routeBilling.getStatistics`:
|
||||
- Pfad-Param `{period}` raus, neuer Pfad `GET /api/billing/statistics`.
|
||||
- Neue Query-Params `dateFrom: date = Query(...)`, `dateTo: date = Query(...)`,
|
||||
`bucketSize: Literal['day','month','year'] = Query(...)`.
|
||||
- Range-Berechnung Z. 570-582 ersetzen durch `parseIsoDateRange`.
|
||||
- `UsageReportResponse.period: str` → entfernen oder umbenennen zu
|
||||
`bucketSize`. Anderen Felder bleiben.
|
||||
- [ ] `routeBilling.getUserViewStatistics`:
|
||||
- Query-Params analog. `period` und `month`/`year` raus.
|
||||
- `bucketSize` an `getTransactionStatisticsAggregated` weitergeben.
|
||||
|
||||
### D. Frontend (~2 h)
|
||||
|
||||
- [ ] `billingApi.fetchStatistics` neu signiert:
|
||||
`fetchStatistics(request, { dateFrom, dateTo, bucketSize })`. Alte
|
||||
Signatur weg.
|
||||
- [ ] `billingApi.fetchViewStatistics` analog.
|
||||
- [ ] `useBilling.loadStatistics({ dateFrom, dateTo, bucketSize })` -
|
||||
bestehende Aufrufer auf neue Signatur umstellen.
|
||||
- [ ] `BillingDashboard.tsx`:
|
||||
- `selectedPeriod`, `selectedYear`, `selectedMonth` State raus.
|
||||
- `<PeriodPicker direction="past" defaultPreset={{ kind: 'thisMonth' }}
|
||||
enabledPresets={['thisMonth','lastMonth','thisQuarter','lastQuarter','ytd','lastYear','last12Months','lastN','custom']} />`.
|
||||
- Separater kleiner `<select>` `bucketSize` (Tag/Monat/Jahr) mit
|
||||
Frontend-Heuristik als Default (z.B. range <= 60 Tage → 'day',
|
||||
<= 24 Monate → 'month', sonst 'year'); user kann uebersteuern.
|
||||
- [ ] `BillingDataView.tsx`: `periodSelectorConfig` raus,
|
||||
`dateRangeSelector={{ enabled: true, direction: 'past', defaultPresetKind: 'thisMonth' }}`
|
||||
rein. Bucket-Size-Toggle separat im Toolbar (eigene
|
||||
`FormGeneratorReport`-Erweiterung oder ausserhalb).
|
||||
- [ ] `ComplianceAuditPage.tsx`: `_periodToDays` und das `_DEFAULT_STATS_PRESET`
|
||||
raus, `_loadStats({ dateFrom, dateTo })`. PeriodPicker-onChange uebergibt
|
||||
direkt `next.fromDate` / `next.toDate`.
|
||||
|
||||
### E. Tests (~1 h)
|
||||
|
||||
- [ ] `gateway/tests/test_routeAudit.py` aktualisieren: alte `timeRange`-Tests
|
||||
ersetzen durch `dateFrom`/`dateTo`-Tests (positiv + 400 bei from>to + 400
|
||||
bei missing).
|
||||
- [ ] `gateway/tests/test_routeBilling.py` aktualisieren: alte `period+year+month`-Tests
|
||||
ersetzen.
|
||||
- [ ] Unit-Tests fuer `dateRange.py`-Helper (Parsing, Timezone, Inklusivitaet
|
||||
des letzten Tages).
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
| # | Kriterium (Given-When-Then) | Prio |
|
||||
|---|---------------------------|------|
|
||||
| 1 | **Given** ein User auf `BillingDashboard` **when** er im PeriodPicker "01.03.2026 - 15.04.2026" waehlt **then** zeigen die Charts genau diesen Bereich, mit korrektem `bucketSize`-Toggle daneben. | must |
|
||||
| 2 | **Given** `GET /api/billing/statistics?dateFrom=2026-04-01&dateTo=2026-04-15&bucketSize=day` **then** liefert genau Transaktionen vom 01.04 00:00 bis 15.04 23:59:59 lokal, in Tages-Buckets. | must |
|
||||
| 3 | **Given** `GET /api/billing/statistics?dateFrom=2026-04-15&dateTo=2026-04-01&bucketSize=day` **then** Antwort HTTP 400 mit Message "dateFrom must be <= dateTo". | must |
|
||||
| 4 | **Given** `GET /api/billing/statistics?dateFrom=2026-04-01&dateTo=2026-04-15` (kein `bucketSize`) **then** Antwort HTTP 400. | must |
|
||||
| 5 | **Given** `GET /api/audit/stats?dateFrom=2026-04-01&dateTo=2026-04-03` **then** Antwort enthaelt `dateFrom`, `dateTo`, `days=3`, KPIs nur fuer diese 3 Tage. | must |
|
||||
| 6 | **Given** ein alter Aufruf `GET /api/audit/stats?timeRange=7` ohne neue Params **then** Antwort HTTP 422 (unknown query param) - Legacy-Param ist explizit entfernt. | must |
|
||||
| 7 | **Given** ein User im `ComplianceAuditPage` waehlt "Custom: 01.04 - 03.04" **then** sieht er KPIs fuer genau 3 Tage; der Adapter `_periodToDays` existiert nicht mehr im Code. | must |
|
||||
| 8 | **Given** ein PeriodPicker-Range "letzte 24 Monate" **when** der User im `BillingDashboard` `bucketSize=year` waehlt **then** zeigt das Chart 3 Jahresbalken. | should |
|
||||
|
||||
## Testplan
|
||||
|
||||
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|
||||
|----|----|-----|--------------|-----------|--------|
|
||||
| T1 | 2 | api | ja | `gateway/tests/test_routeBilling.py::test_getStatistics_dateRange_dayBucket` | pending |
|
||||
| T2 | 3 | api | ja | `gateway/tests/test_routeBilling.py::test_getStatistics_invertedRange_400` | pending |
|
||||
| T3 | 4 | api | ja | `gateway/tests/test_routeBilling.py::test_getStatistics_missingBucketSize_400` | pending |
|
||||
| T4 | 5 | api | ja | `gateway/tests/test_routeAudit.py::test_getAuditStats_dateRange` | pending |
|
||||
| T5 | 6 | api | ja | `gateway/tests/test_routeAudit.py::test_getAuditStats_legacyParam_rejected` | pending |
|
||||
| T6 | - | unit | ja | `gateway/tests/test_dateRange.py::test_inclusive_endOfDay` | pending |
|
||||
| T7 | 1, 8 | manuell | nein | `BillingDashboard` smoke | pending |
|
||||
| T8 | 7 | manuell | nein | `ComplianceAuditPage` smoke + grep `_periodToDays` muss leer sein | pending |
|
||||
|
||||
## Reihenfolge der Umsetzung
|
||||
|
||||
Der Clean-Cut zwingt zu einer atomaren PR (Backend + alle Caller in einem
|
||||
Commit). Empfohlene lokale Arbeits-Reihenfolge:
|
||||
|
||||
1. A: `dateRange`-Helper inkl. Tests.
|
||||
2. B: Audit-Backend.
|
||||
3. C: Billing-Backend.
|
||||
4. D: Frontend (alle drei Pages parallel).
|
||||
5. E: Tests gruen, manueller Smoke aller drei Pages.
|
||||
6. PR.
|
||||
|
||||
## Links
|
||||
|
||||
- Vorarbeit: PeriodPicker-Komponente + Schritt-1+2-Rollout (Changelog
|
||||
2026-04-20)
|
||||
- Code-Anker:
|
||||
- `gateway/modules/routes/routeAudit.py:303`
|
||||
- `gateway/modules/shared/aiAuditLogger.py:195`
|
||||
- `gateway/modules/routes/routeBilling.py:526`
|
||||
- `gateway/modules/routes/routeBilling.py:1619`
|
||||
- `frontend_nyla/src/pages/billing/BillingDashboard.tsx` (`selectedPeriod`)
|
||||
- `frontend_nyla/src/pages/billing/BillingDataView.tsx` (`_loadViewStatistics`)
|
||||
- `frontend_nyla/src/pages/ComplianceAuditPage.tsx` (`_periodToDays`)
|
||||
|
||||
## Abschluss
|
||||
|
||||
- [ ] `b-reference/gateway/billing.md` Statistik-Section neu schreiben
|
||||
(`dateFrom`/`dateTo`/`bucketSize`).
|
||||
- [ ] `b-reference/platform/audit.md` Stats-Section neu schreiben.
|
||||
- [ ] TOPICS.md - keine Aenderung noetig.
|
||||
- [ ] Dieses Dokument → `z-archive/` verschieben.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Google OAuth 2.0 Setup Guide for PowerOn
|
||||
# Google OAuth 2.0 Setup Guide for Porta
|
||||
|
||||
## Overview
|
||||
This guide explains how to set up Google OAuth 2.0 authentication for the PowerOn application.
|
||||
This guide explains how to set up Google OAuth 2.0 authentication for the Porta application.
|
||||
|
||||
## Prerequisites
|
||||
- A Google account
|
||||
|
|
@ -12,7 +12,7 @@ This guide explains how to set up Google OAuth 2.0 authentication for the PowerO
|
|||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Click on the project dropdown at the top of the page
|
||||
3. Click "New Project"
|
||||
4. Enter a project name (e.g., "PowerOn OAuth")
|
||||
4. Enter a project name (e.g., "Porta OAuth")
|
||||
5. Click "Create"
|
||||
|
||||
## Step 2: Configure OAuth Consent Screen
|
||||
|
|
@ -33,7 +33,7 @@ This guide explains how to set up Google OAuth 2.0 authentication for the PowerO
|
|||
|
||||
4. Back to creating OAuth client ID:
|
||||
- Application type: "Web application"
|
||||
- Name: "PowerOn Web Client"
|
||||
- Name: "Porta Web Client"
|
||||
- Authorized redirect URIs: Add **two** redirect URIs per environment:
|
||||
- Login callback: `http://localhost:8000/api/google/auth/login/callback`
|
||||
- Data connect callback: `http://localhost:8000/api/google/auth/connect/callback`
|
||||
|
|
@ -42,7 +42,7 @@ This guide explains how to set up Google OAuth 2.0 authentication for the PowerO
|
|||
5. Click "Create"
|
||||
6. **Important**: Copy the Client ID and Client Secret - you'll need these for the next step
|
||||
|
||||
## Step 4: Configure PowerOn Application
|
||||
## Step 4: Configure Porta Application
|
||||
|
||||
1. Open your environment file (`gateway/env_dev.env` for development)
|
||||
2. Replace the placeholder values with your actual Google OAuth credentials:
|
||||
|
|
@ -62,15 +62,15 @@ Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect
|
|||
> **Hinweis:** Das Gateway unterscheidet zwei OAuth-Apps: eine fuer Login (Auth) und eine fuer Daten-Connections (Data). Details: `routeSecurityGoogle.py`.
|
||||
|
||||
3. Save the file
|
||||
4. Restart your PowerOn gateway server
|
||||
4. Restart your Porta gateway server
|
||||
|
||||
## Step 5: Test the Configuration
|
||||
|
||||
1. Start your PowerOn application
|
||||
1. Start your Porta application
|
||||
2. Go to the Connections module
|
||||
3. Click "Connect Google"
|
||||
4. You should be redirected to Google's OAuth consent screen
|
||||
5. After authorization, you should be redirected back to PowerOn
|
||||
5. After authorization, you should be redirected back to Porta
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ Service_GOOGLE_DATA_REDIRECT_URI = http://localhost:8000/api/google/auth/connect
|
|||
|
||||
### Debug Steps
|
||||
|
||||
1. Check the PowerOn gateway logs for OAuth configuration details
|
||||
1. Check the Porta gateway logs for OAuth configuration details
|
||||
2. Verify environment variables are loaded correctly
|
||||
3. Ensure the Google OAuth client is configured for "Web application" type
|
||||
4. Check that the redirect URI includes the full path: `/api/google/auth/callback`
|
||||
|
|
@ -115,7 +115,7 @@ For production deployment:
|
|||
|
||||
If you continue to experience issues:
|
||||
|
||||
1. Check the PowerOn gateway logs for detailed error messages
|
||||
1. Check the Porta gateway logs for detailed error messages
|
||||
2. Verify your Google OAuth configuration in Google Cloud Console
|
||||
3. Test with a simple OAuth flow to isolate the issue
|
||||
4. Ensure your Google Cloud project has billing enabled (required for some APIs)
|
||||
|
|
|
|||
390
d-guides/google-oauth-verification.md
Normal file
390
d-guides/google-oauth-verification.md
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
# Google OAuth Verification — Boilerplates für Porta
|
||||
|
||||
> Begleitdokument zu `google-oauth-setup.md`. Enthält copy-paste-fähige
|
||||
> Bausteine für die Google Trust-&-Safety-Verifizierung der **Porta**-App
|
||||
> (`porta.poweron.swiss`, Developer: `p.motsch@poweron.swiss`,
|
||||
> GCP-Projekt `<GCP-Neu-Generieren>`).
|
||||
|
||||
## App-Daten (zur konsistenten Wiederverwendung)
|
||||
|
||||
| Feld | Wert |
|
||||
|----------------------------|-----------------------------------------------------------------------------------------------|
|
||||
| App-Name | Porta |
|
||||
| Anbieter (Legal Entity) | PowerOn AG |
|
||||
| Verifizierte Domain | `poweron.swiss` |
|
||||
| Homepage-URL | `https://porta.poweron.swiss/poweron-home.html` |
|
||||
| Privacy-Policy-URL | `https://porta.poweron.swiss/poweron-privacy.html` |
|
||||
| Terms-of-Service-URL | `https://porta.poweron.swiss/poweron-terms.html` |
|
||||
| Support-E-Mail | `support@poweron.swiss` |
|
||||
| Developer-Kontakt | `p.motsch@poweron.swiss` |
|
||||
| GCP-Projekt-Nummer | `<GCP-Neu-Generieren>` |
|
||||
| GCP-Projekt-ID | (siehe `env_prod.env`, `Service_GOOGLE_DATA_CLIENT_ID`) |
|
||||
| Verwendete Google-Scopes | `openid`, `userinfo.email`, `userinfo.profile`, `gmail.readonly`, `drive.readonly` |
|
||||
|
||||
> Bei jeder Änderung an Scopes (`gateway/modules/auth/oauthProviderConfig.py::googleDataScopes`)
|
||||
> diese Tabelle, die OAuth-Consent-Screen-Konfiguration in der GCP-Console
|
||||
> **und** die Privacy-Policy synchron halten — Google bouncet sonst die
|
||||
> Verification.
|
||||
|
||||
---
|
||||
|
||||
## 1. Privacy-Policy-Sektion zu Google-Daten
|
||||
|
||||
Die folgenden zwei Sektionen müssen **wortgleich** auf `https://porta.poweron.swiss/poweron-privacy.html`
|
||||
unter denselben Domain-/HTTPS-Bedingungen erreichbar sein wie die Homepage.
|
||||
Beide Sprachen anbieten (deutsche und englische Fassung), englische Fassung ist
|
||||
für Google maßgeblich.
|
||||
|
||||
### 1.1 Deutsche Fassung
|
||||
|
||||
```markdown
|
||||
## Verarbeitung von Daten aus Google-Diensten
|
||||
|
||||
Porta ist eine Plattform für KI-gestützte Geschäftsworkflows. Wenn Sie eine
|
||||
Verbindung zu Ihrem Google-Konto herstellen, greift Porta ausschließlich auf
|
||||
diejenigen Daten zu, die für die von Ihnen ausgewählte Funktionalität
|
||||
erforderlich sind, und ausschließlich mit den von Ihnen über den Google-OAuth-
|
||||
Consent-Bildschirm freigegebenen Berechtigungen (Scopes).
|
||||
|
||||
### Welche Google-Berechtigungen wir anfordern
|
||||
|
||||
| Scope | Wozu Porta diese Berechtigung nutzt |
|
||||
|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `openid`, `userinfo.email`, `userinfo.profile` | Anmeldung an Porta (Single-Sign-On) und eindeutige Zuordnung Ihres Benutzerkontos. |
|
||||
| `https://www.googleapis.com/auth/drive.readonly` | Lesezugriff auf Ihre Google-Drive-Dateien, damit Sie Dateien aus Drive im Porta-Workspace auswählen, vorschauen und der KI als Quelle übergeben können. Porta schreibt **nicht** in Ihr Drive. |
|
||||
| `https://www.googleapis.com/auth/gmail.readonly` | Lesezugriff auf Ihre Gmail-Labels und -Nachrichten, damit Sie E-Mails in Porta auflisten, vorschauen und einer KI-Konversation als Quelle anhängen können. Porta versendet, ändert oder löscht **keine** E-Mails. |
|
||||
|
||||
### Was mit den Daten geschieht
|
||||
|
||||
* **Speicherung**: Porta speichert die OAuth-Refresh-Tokens verschlüsselt in
|
||||
der eigenen Datenbank, um ohne erneuten Login auf Ihre freigegebenen Daten
|
||||
zugreifen zu können. Inhalte aus Drive oder Gmail werden nur dann persistiert,
|
||||
wenn Sie sie aktiv als „Quelle" oder „Datei" in Porta anlegen; ansonsten
|
||||
werden sie nur transient für die jeweilige Anfrage geladen.
|
||||
* **Verwendung**: Drive- und Gmail-Daten werden ausschließlich genutzt, um
|
||||
Ihre direkten Anfragen in Porta zu beantworten (z.B. Dokument anzeigen,
|
||||
durchsuchen, einer KI-Konversation beifügen, automatisierte Workflows
|
||||
ausführen, die Sie selbst konfiguriert haben).
|
||||
* **Keine Weitergabe**: Porta gibt Daten aus Ihrem Google-Konto **nicht** an
|
||||
Dritte weiter, mit Ausnahme von Sub-Prozessoren, die für den Betrieb der
|
||||
Plattform notwendig sind (Cloud-Hosting, KI-Inferenz). Diese Sub-Prozessoren
|
||||
sind in Anhang A dieser Richtlinie aufgeführt und vertraglich auf den Schutz
|
||||
Ihrer Daten verpflichtet.
|
||||
* **Keine Werbung, kein Verkauf, kein Modelltraining**: Porta nutzt Daten
|
||||
aus Ihrem Google-Konto **nicht** für personalisierte Werbung, **nicht** für
|
||||
den Verkauf an Dritte und **nicht** für das Training generischer KI-Modelle.
|
||||
* **Menschlicher Zugriff**: Auf Ihre Google-Daten wird kein routinemäßiger
|
||||
menschlicher Zugriff genommen. Mitarbeiter der PowerOn AG können nur in den
|
||||
folgenden eng begrenzten Fällen Einsicht nehmen: (a) mit Ihrer ausdrücklichen
|
||||
Einwilligung, (b) zur Sicherheitsuntersuchung eines konkreten Vorfalls,
|
||||
(c) zur Erfüllung gesetzlicher Verpflichtungen, oder (d) wo dies zur Behebung
|
||||
eines technischen Fehlers in einer einzelnen Anfrage zwingend nötig ist.
|
||||
|
||||
### Limited Use Disclosure
|
||||
|
||||
Porta hält die Anforderungen der **Google API Services User Data Policy**
|
||||
einschließlich der **Limited-Use-Anforderungen** ein.
|
||||
|
||||
> *Porta's use and transfer of information received from Google APIs to any
|
||||
> other app will adhere to Google API Services User Data Policy, including the
|
||||
> Limited Use requirements.*
|
||||
> *(<https://developers.google.com/terms/api-services-user-data-policy>)*
|
||||
|
||||
### Verbindung trennen und Daten löschen
|
||||
|
||||
Sie können Ihre Google-Verbindung jederzeit beenden:
|
||||
|
||||
1. **In Porta**: Profil → Verbindungen → Google → „Trennen". Damit werden die
|
||||
bei Porta gespeicherten OAuth-Tokens unverzüglich widerrufen und gelöscht.
|
||||
2. **In Ihrem Google-Konto**: <https://myaccount.google.com/permissions> →
|
||||
Porta auswählen → „Zugriff entfernen".
|
||||
|
||||
Auf Anfrage an `support@poweron.swiss` löschen wir innerhalb von 30 Tagen
|
||||
sämtliche von Ihrem Google-Konto stammende Inhalte aus unseren Systemen
|
||||
(unbeschadet gesetzlicher Aufbewahrungspflichten).
|
||||
```
|
||||
|
||||
### 1.2 English version
|
||||
|
||||
```markdown
|
||||
## Processing of Data from Google Services
|
||||
|
||||
Porta is a platform for AI-assisted business workflows. When you connect
|
||||
your Google account, Porta accesses only the data that is required for the
|
||||
functionality you have selected, and only within the permissions (scopes) you
|
||||
have explicitly granted on the Google OAuth consent screen.
|
||||
|
||||
### Google permissions we request
|
||||
|
||||
| Scope | How Porta uses this permission |
|
||||
|----------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `openid`, `userinfo.email`, `userinfo.profile` | Sign-in to Porta (Single Sign-On) and unambiguous identification of your user account. |
|
||||
| `https://www.googleapis.com/auth/drive.readonly` | Read-only access to your Google Drive files, so that you can browse, preview and attach Drive files as sources to AI conversations and workflows in Porta. Porta does **not** write to your Drive. |
|
||||
| `https://www.googleapis.com/auth/gmail.readonly` | Read-only access to your Gmail labels and messages, so that you can list, preview and attach emails as sources to AI conversations in Porta. Porta does **not** send, modify or delete emails. |
|
||||
|
||||
### How we handle the data
|
||||
|
||||
* **Storage**: Porta stores OAuth refresh tokens encrypted in its own
|
||||
database in order to access your authorized data without forcing you to
|
||||
re-authenticate. Content from Drive or Gmail is persisted only when you
|
||||
actively register it as a "source" or "file" in Porta; otherwise it is
|
||||
loaded transiently for the duration of the request only.
|
||||
* **Usage**: Drive and Gmail data are used exclusively to fulfill your direct
|
||||
requests within Porta (e.g., display, search, attach to an AI
|
||||
conversation, run an automated workflow you yourself have configured).
|
||||
* **No third-party sharing**: Porta does **not** share your Google data
|
||||
with third parties, except sub-processors strictly necessary to operate the
|
||||
platform (cloud hosting, AI inference). Sub-processors are listed in
|
||||
Annex A of this policy and are contractually bound to protect your data.
|
||||
* **No advertising, no sale, no model training**: Porta does **not** use
|
||||
your Google account data for personalized advertising, **not** for sale to
|
||||
third parties, and **not** to train generalized AI models.
|
||||
* **Human access**: Routine human access to your Google data does not occur.
|
||||
PowerOn AG personnel may access your Google data only in narrowly defined
|
||||
cases: (a) with your explicit consent, (b) for security investigation of a
|
||||
specific incident, (c) to comply with applicable law, or (d) where strictly
|
||||
necessary to fix a technical defect in a specific request.
|
||||
|
||||
### Limited Use Disclosure
|
||||
|
||||
Porta complies with the requirements of the **Google API Services User
|
||||
Data Policy**, including the **Limited Use** requirements.
|
||||
|
||||
> *Porta's use and transfer of information received from Google APIs to any
|
||||
> other app will adhere to the Google API Services User Data Policy,
|
||||
> including the Limited Use requirements.*
|
||||
> *(<https://developers.google.com/terms/api-services-user-data-policy>)*
|
||||
|
||||
### Disconnecting and deleting your data
|
||||
|
||||
You may revoke your Google connection at any time:
|
||||
|
||||
1. **In Porta**: Profile → Connections → Google → "Disconnect". This
|
||||
immediately revokes and deletes the OAuth tokens stored at Porta.
|
||||
2. **In your Google account**: <https://myaccount.google.com/permissions> →
|
||||
select Porta → "Remove access".
|
||||
|
||||
On request to `support@poweron.swiss`, we will delete all content originating
|
||||
from your Google account from our systems within 30 days (subject to
|
||||
statutory retention obligations).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Justifications für jeden Scope (Verification-Formular)
|
||||
|
||||
Google verlangt im Antrag pro Scope eine konkrete Begründung. **Wörtlich
|
||||
einkopierbar** in das Antragsformular:
|
||||
|
||||
### 2.1 `openid`, `userinfo.email`, `userinfo.profile`
|
||||
|
||||
> Required to authenticate the user into Porta via Google Single Sign-On
|
||||
> and to identify their account uniquely. We display the user's email and
|
||||
> name within the application; we do not use these for any other purpose.
|
||||
|
||||
### 2.2 `https://www.googleapis.com/auth/drive.readonly`
|
||||
|
||||
> Porta provides a unified data source panel where users can browse their
|
||||
> own Google Drive folders and attach selected Drive files (e.g., contracts,
|
||||
> reports, spreadsheets) as inputs to AI-driven conversations and to
|
||||
> automation workflows they configure themselves. We need read-only access
|
||||
> in order to (a) list folders and files the user owns or has shared access
|
||||
> to, (b) display file metadata such as name, type and modification date in
|
||||
> our source picker, and (c) download the file content on demand when the
|
||||
> user explicitly attaches a file to a chat or a workflow step. We do not
|
||||
> request write or full-Drive access; Porta never modifies or deletes the
|
||||
> user's Drive content.
|
||||
|
||||
### 2.3 `https://www.googleapis.com/auth/gmail.readonly`
|
||||
|
||||
> Porta lets users attach individual Gmail messages as conversation
|
||||
> sources to its AI assistant (for example: "summarize this email",
|
||||
> "extract the invoice data from this thread") and use email content as
|
||||
> input to user-configured automation workflows. We need read-only access
|
||||
> in order to (a) list the user's labels in our source picker, (b) list
|
||||
> messages within a chosen label with metadata (subject, sender, date), and
|
||||
> (c) fetch a specific message body and attachments when the user
|
||||
> explicitly selects it. We do not request modify, send or full-mailbox
|
||||
> access; Porta never sends, alters or deletes the user's email.
|
||||
|
||||
---
|
||||
|
||||
## 3. Demo-Video — Drehbuch
|
||||
|
||||
**Format**: YouTube unlisted, Bildschirmaufzeichnung mit Voice-Over (oder
|
||||
On-Screen-Text), Länge **2:30 – 4:00 min**, 1080p, Englisch (Google reviewt
|
||||
auf Englisch). Mauszeiger sichtbar, ruhiger Cursor.
|
||||
|
||||
**Vor Aufnahme prüfen**: App in einem **frischen Inkognito-Fenster** öffnen,
|
||||
mit einem **echten Google-Account** (nicht „test@…"), in der URL-Leiste
|
||||
muss `porta.poweron.swiss` deutlich lesbar sein.
|
||||
|
||||
### Szene-für-Szene-Skript
|
||||
|
||||
#### Szene 1 — Homepage & App-Identität (0:00 – 0:25)
|
||||
|
||||
* **Bild**: Browser zeigt `https://porta.poweron.swiss`. Logo, App-Name
|
||||
„Porta", kurzer Marketing-Text sichtbar.
|
||||
* **Voice / On-Screen**:
|
||||
> *"This is Porta, available at porta.poweron.swiss. Porta is a
|
||||
> platform for AI-assisted business workflows. The OAuth client ID we are
|
||||
> submitting for verification belongs to this application."*
|
||||
|
||||
#### Szene 2 — Sign-in starten (0:25 – 0:45)
|
||||
|
||||
* **Bild**: User klickt „Sign in with Google".
|
||||
* **Voice / On-Screen**:
|
||||
> *"To use Porta, the user signs in with their Google account."*
|
||||
|
||||
#### Szene 3 — OAuth-Consent-Screen (0:45 – 1:15)
|
||||
|
||||
* **Bild**: Google-Consent-Bildschirm öffnet sich. **Klar lesbar**:
|
||||
App-Name „Porta", angeforderte Scopes (alle, einzeln aufgeführt),
|
||||
Privacy-Policy- und ToS-Link.
|
||||
* **Voice / On-Screen**:
|
||||
> *"Here Google shows the consent screen for Porta. The user can review
|
||||
> each requested permission individually: read access to Google Drive,
|
||||
> read access to Gmail, and basic profile information. The user grants
|
||||
> consent."*
|
||||
* User klickt „Continue" / „Allow".
|
||||
|
||||
#### Szene 4 — Inside the app (1:15 – 1:30)
|
||||
|
||||
* **Bild**: Nach erfolgreichem Login zeigt Porta das Workspace-Dashboard
|
||||
mit dem User-Namen oben rechts.
|
||||
* **Voice / On-Screen**:
|
||||
> *"After consent, the user is redirected back to Porta and is signed
|
||||
> in."*
|
||||
|
||||
#### Szene 5 — Drive-Scope in action (1:30 – 2:15)
|
||||
|
||||
* **Bild**:
|
||||
1. User öffnet die UDB („Unified Data Bar") → Tab „Sources".
|
||||
2. Wählt die Google-Connection.
|
||||
3. Klickt auf „Drive". Die Liste der Drive-Ordner und -Dateien
|
||||
erscheint.
|
||||
4. User wählt eine Datei (z.B. ein PDF) und klickt „Attach to chat".
|
||||
5. Im Chat-Eingabefeld erscheint die Datei als Anhang-Chip. User tippt
|
||||
"Summarize this document" und sendet.
|
||||
6. Die KI-Antwort referenziert den Inhalt der Datei.
|
||||
* **Voice / On-Screen**:
|
||||
> *"Porta uses the Drive read-only scope to let the user browse their
|
||||
> Drive, attach a specific file as a source for the AI conversation, and
|
||||
> then have the AI process exactly that file. Porta does not modify or
|
||||
> delete anything in the user's Drive."*
|
||||
|
||||
#### Szene 6 — Gmail-Scope in action (2:15 – 3:00)
|
||||
|
||||
* **Bild**:
|
||||
1. Zurück in „Sources" → Google-Connection → Klick auf „Gmail".
|
||||
2. Die Liste der Gmail-Labels („INBOX", „SENT", eigene Labels)
|
||||
erscheint.
|
||||
3. User öffnet ein Label, eine Liste von Mails (Subject, From, Date)
|
||||
wird angezeigt.
|
||||
4. User wählt eine Mail und klickt „Attach to chat".
|
||||
5. Im Chat erscheint die Mail als Quelle. User tippt z.B. „Extract the
|
||||
order details from this email" und sendet.
|
||||
6. Die KI-Antwort zeigt strukturierte Felder aus der Mail.
|
||||
* **Voice / On-Screen**:
|
||||
> *"The Gmail read-only scope is used so the user can pick a specific
|
||||
> email to feed into an AI conversation. Porta lists labels and
|
||||
> messages and fetches the body only of the message the user explicitly
|
||||
> selects. Porta never sends, modifies or deletes email."*
|
||||
|
||||
#### Szene 7 — Disconnect (3:00 – 3:30)
|
||||
|
||||
* **Bild**: User öffnet Profil → Connections → Google-Eintrag → klickt
|
||||
„Disconnect". Bestätigungsdialog. Der Eintrag verschwindet.
|
||||
* **Voice / On-Screen**:
|
||||
> *"At any time, the user can disconnect their Google account in
|
||||
> Porta. This immediately revokes and deletes the stored OAuth
|
||||
> tokens."*
|
||||
|
||||
#### Szene 8 — Privacy Policy (3:30 – 3:50)
|
||||
|
||||
* **Bild**: Browser navigiert zu `https://porta.poweron.swiss/poweron-privacy.html`.
|
||||
Scrollen zur Sektion „Processing of Data from Google Services" und zur
|
||||
„Limited Use Disclosure".
|
||||
* **Voice / On-Screen**:
|
||||
> *"Our privacy policy at porta.poweron.swiss/poweron-privacy.html explicitly lists
|
||||
> each Google scope, the purpose of each scope, and includes the Limited
|
||||
> Use Disclosure as required by the Google API Services User Data Policy."*
|
||||
|
||||
#### Szene 9 — Outro (3:50 – 4:00)
|
||||
|
||||
* **Bild**: Porta-Logo, App-Name, Submission-Datum.
|
||||
* **Voice / On-Screen**:
|
||||
> *"Thank you for reviewing Porta for OAuth verification."*
|
||||
|
||||
### Aufnahme-Checkliste
|
||||
|
||||
* [ ] URL-Bar in jedem Frame zeigt `porta.poweron.swiss` (kein localhost,
|
||||
kein staging).
|
||||
* [ ] Browser-Inkognito, keine Browser-Extensions sichtbar.
|
||||
* [ ] Consent-Screen zeigt korrekten App-Namen „Porta" und korrektes Logo.
|
||||
* [ ] Jeder im Antrag deklarierte Scope ist in mindestens einer Szene
|
||||
*aktiv genutzt* zu sehen (nicht nur erwähnt).
|
||||
* [ ] Kein „Lorem-ipsum", keine Demo-Daten mit „test", „dummy", „foo".
|
||||
* [ ] Audio sauber oder Sprache als On-Screen-Text eingeblendet.
|
||||
* [ ] Video als **unlisted** auf YouTube hochladen, Link ins
|
||||
Verification-Formular eintragen.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pre-Submission-Checkliste
|
||||
|
||||
Vor dem (erneuten) Klick auf „Submit for verification" alles abhaken:
|
||||
|
||||
* [ ] Domain `poweron.swiss` in der **Google Search Console** verifiziert
|
||||
(DNS-TXT, gleicher Google-Account wie OAuth-Owner).
|
||||
* [ ] `https://porta.poweron.swiss` öffentlich erreichbar, HTTPS, kein
|
||||
Login-Wall, beschreibt klar was Porta ist.
|
||||
* [ ] `https://porta.poweron.swiss/poweron-privacy.html` öffentlich erreichbar,
|
||||
enthält die beiden Sektionen aus Kapitel 1 dieses Dokuments.
|
||||
* [ ] `https://porta.poweron.swiss/poweron-terms.html` öffentlich erreichbar.
|
||||
* [ ] OAuth-Consent-Screen in der GCP-Console:
|
||||
* App-Name „Porta", App-Logo (120×120 PNG, kein Google-Branding).
|
||||
* Support-E-Mail `support@poweron.swiss`.
|
||||
* Authorized domain: `poweron.swiss`.
|
||||
* Application home page: `https://porta.poweron.swiss`.
|
||||
* Application privacy policy link: `https://porta.poweron.swiss/poweron-privacy.html`.
|
||||
* Application terms of service link: `https://porta.poweron.swiss/poweron-terms.html`.
|
||||
* [ ] Scope-Liste im Consent-Screen identisch zu `googleDataScopes` in
|
||||
`gateway/modules/auth/oauthProviderConfig.py`.
|
||||
* [ ] Jeder Scope hat eine Justification (Kapitel 2 dieses Dokuments).
|
||||
* [ ] Demo-Video (Kapitel 3) auf YouTube unlisted, Link im Antrag.
|
||||
* [ ] Developer-E-Mail `p.motsch@poweron.swiss` empfangsbereit; Eingangs-Mail
|
||||
vom Trust-&-Safety-Team kommt erfahrungsgemäß innerhalb 3-5 Tagen.
|
||||
|
||||
---
|
||||
|
||||
## 5. Was tun, wenn Google rejected
|
||||
|
||||
Erfahrungsgemäß werden mindestens 1-2 Iterationen bis zur Approval nötig.
|
||||
Häufigste Reject-Gründe und Fixes:
|
||||
|
||||
| Reject-Grund | Fix |
|
||||
|--------------------------------------------------------|------------------------------------------------------------------------------------------------------|
|
||||
| „Privacy policy does not enumerate all requested scopes" | Sektion 1.2 dieses Dokuments wörtlich übernehmen, jeden Scope aus Tabelle erwähnen. |
|
||||
| „Demo video does not show scope X being used" | Eine zusätzliche Szene im Video, in der Scope X aktiv ausgeübt wird (siehe Szene 5/6). |
|
||||
| „Homepage does not describe what the app does" | Auf `porta.poweron.swiss` einen klaren „What is Porta"-Block oberhalb des Login-Buttons. |
|
||||
| „Brand verification — domain not verified" | Domain in Google Search Console verifizieren mit demselben Account wie OAuth-Owner. |
|
||||
| „Limited Use disclosure missing" | Wörtliche Limited-Use-Klausel aus Sektion 1.2 in die Privacy-Policy aufnehmen. |
|
||||
| „App requests scopes broader than necessary" | Prüfen ob `gmail.readonly` durch `gmail.metadata` ersetzt werden kann (nicht für Porta — wir lesen Inhalte). Begründen. |
|
||||
|
||||
Bei jedem Re-Submit nur die beanstandeten Punkte fixen, kein „While we're
|
||||
at it" — das verlängert den Loop.
|
||||
|
||||
---
|
||||
|
||||
## 6. Wenn Verification durch ist
|
||||
|
||||
* GCP-Console → OAuth Consent Screen → Publishing Status
|
||||
„**In Production**".
|
||||
* Test-Users-Liste kann geleert werden (nicht nötig zu entfernen, schadet
|
||||
aber auch nicht).
|
||||
* Externe User sehen ab sofort keinen Warn-Bildschirm mehr, nur den
|
||||
normalen, signierten Consent-Screen mit App-Logo.
|
||||
* Bei jeder zukünftigen Scope-Änderung in `googleDataScopes`: erneut
|
||||
durch Verification, **mit derselben Vorgehensweise**. Daher Scopes
|
||||
bewusst minimal halten.
|
||||
155
d-guides/stripe-ch-vat.md
Normal file
155
d-guides/stripe-ch-vat.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<!-- status: canonical -->
|
||||
<!-- lastReviewed: 2026-04-20 (rev 2: structured invoice fields) -->
|
||||
|
||||
# Stripe-Rechnungen CH-Treuhand-konform aufsetzen
|
||||
|
||||
Schweizer Treuhand-Praxis verlangt fuer jede PowerOn-Rechnung (Top-Up &
|
||||
Subscription) drei Dinge, die Stripe nicht von sich aus liefert:
|
||||
|
||||
1. **MWST 8.1 % (CH)** muss separat ausgewiesen werden -- "obendrauf", nicht
|
||||
inkludiert.
|
||||
2. **Vollstaendige Empfaenger-Adresse** (Firma, Strasse, PLZ/Ort, Land,
|
||||
optional UID-Nr.) muss auf der Rechnung erscheinen.
|
||||
3. **Zahlungsstatus** ("bereits bezahlt via Kreditkarte") muss vermerkt
|
||||
sein -- die reine Stripe-Quittung reicht nicht aus.
|
||||
|
||||
Der Code-Pfad in PowerOn wurde so vorbereitet, dass nur noch die
|
||||
Stripe-Konfiguration gemacht werden muss; danach werden alle neuen
|
||||
Top-Up-Rechnungen automatisch konform erstellt.
|
||||
|
||||
---
|
||||
|
||||
## 1. App-seitig: was ist bereits implementiert (Stand 2026-04-20)
|
||||
|
||||
| Bereich | Datei | Was passiert |
|
||||
|---------|-------|--------------|
|
||||
| Mandant-Modell | `gateway/modules/datamodels/datamodelUam.py` -- `Mandate.invoice*` (10 strukturierte Felder) | Rechnungsadresse als einzelne Felder: `invoiceCompanyName`, `invoiceContactName`, `invoiceEmail`, `invoiceLine1`, `invoiceLine2`, `invoicePostalCode`, `invoiceCity`, `invoiceState`, `invoiceCountry` (default `CH`), `invoiceVatNumber`. Der FormGenerator rendert die Felder ueber `order: 200..209` automatisch gruppiert am Ende des Edit-Formulars. |
|
||||
| Stripe-Customer | `serviceCenter/services/serviceBilling/stripeCheckout.py` -- `_ensureStripeCustomer` | Bei jedem Checkout wird ein Stripe-Customer angelegt/aktualisiert: `name` = `invoiceCompanyName` (Fallback Mandant-Label), `email` = `invoiceEmail`, `address` = strukturiertes `{ line1, line2, postal_code, city, state, country }`, `shipping` = z. H. + Adresse, beim _Create_ ausserdem `tax_id_data` aus `invoiceVatNumber` (CHE -> `ch_vat`, LI -> `li_uid`, EU -> `eu_vat`). Die `stripeCustomerId` wird im `BillingSettings` gecacht. |
|
||||
| Rechnungserzeugung | `create_checkout_session` setzt `invoice_creation: { enabled: true, invoice_data: {...} }` | Stripe erzeugt automatisch eine **Rechnung** (statt nur einer Quittung) mit Status `paid`, der vollstaendigen Customer-Adresse, unserem Footer-Hinweis "bereits via Kreditkarte bezahlt" und (bei vorhandener UID/z.H.) `invoice_data.custom_fields` mit `UID-Nr. Empfaenger` / `z. H.`. |
|
||||
| MWST | `create_checkout_session` -- `automatic_tax` ODER `tax_rates` | Wenn `STRIPE_AUTOMATIC_TAX_ENABLED=true`, wird Stripe Tax verwendet. Andernfalls wird der Tax-Rate aus `STRIPE_TAX_RATE_ID_CH_VAT` an jede Line-Item gehaengt. |
|
||||
|
||||
Das alles passiert pro Top-Up automatisch -- der App-Code ist fertig. **Was
|
||||
zu tun bleibt, ist die Stripe-Dashboard-Konfiguration und das Hinterlegen der
|
||||
Rechnungsadresse pro Mandant.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Stripe-Dashboard: einmalige Einrichtung
|
||||
|
||||
### 2a. MWST 8.1 % aktivieren (Empfehlung: Stripe Tax)
|
||||
|
||||
**Variante A -- Stripe Tax (empfohlen, automatisch):**
|
||||
|
||||
1. Stripe Dashboard -> **Tax** -> Origin address: PowerOn-Hauptsitz
|
||||
(Schweiz) eintragen.
|
||||
2. Tax registrations -> **Switzerland** hinzufuegen, Steuernummer (UID)
|
||||
hinterlegen.
|
||||
3. **Default tax behavior** auf `exclusive` (= "MWST kommt obendrauf").
|
||||
4. **Default tax category** fuer unser Produkt: `Software as a Service
|
||||
(SaaS)` -- damit greift automatisch CH-MWST 8.1 % (Standard-Rate).
|
||||
5. APP_CONFIG / `.env` setzen: `STRIPE_AUTOMATIC_TAX_ENABLED=true`.
|
||||
|
||||
**Variante B -- Manueller Tax-Rate (Fallback ohne Stripe Tax):**
|
||||
|
||||
1. Stripe Dashboard -> **Products** -> **Tax rates** -> **Add tax rate**.
|
||||
- Display name: `MWST CH`
|
||||
- Region: `Switzerland (CH)`
|
||||
- Tax rate: `8.1` %
|
||||
- Inclusive: **NEIN** (= "MWST kommt obendrauf")
|
||||
- Description: `Schweizer Mehrwertsteuer 8.1%`
|
||||
2. Tax-Rate-ID kopieren (Format `txr_...`).
|
||||
3. APP_CONFIG / `.env` setzen:
|
||||
- `STRIPE_AUTOMATIC_TAX_ENABLED=false`
|
||||
- `STRIPE_TAX_RATE_ID_CH_VAT=txr_xxxxxxxxxxxxxx`
|
||||
|
||||
### 2b. Rechnungs-Template
|
||||
|
||||
1. Stripe Dashboard -> **Settings** -> **Invoice template**.
|
||||
2. **Public business name**: `PowerOn` (oder offizieller Firmenname inkl. AG/GmbH).
|
||||
3. **Business address**: vollstaendige Schweizer Geschaeftsadresse.
|
||||
4. **Tax IDs to display**: PowerOn-UID-Nr (z. B. `CHE-123.456.789 MWST`)
|
||||
anhaken -- erscheint dann automatisch im Header jeder Rechnung.
|
||||
5. **Footer / Memo**: ggf. zusaetzlicher Hinweis "Bezahlt via Kreditkarte
|
||||
am Rechnungsdatum" (wir setzen ihn auch im Code als invoice-spezifischen
|
||||
Footer, das hier ist nur Fallback).
|
||||
6. **Default payment terms**: `Due upon receipt` (sollte fuer Top-Ups eh
|
||||
irrelevant sein, da Status sofort `paid`).
|
||||
|
||||
### 2c. Webhook fuer Invoice-Status (optional)
|
||||
|
||||
Wenn Sie wollen, dass die App-DB protokolliert, dass die Rechnung erfolgreich
|
||||
ausgestellt wurde, koennen Sie das Event `invoice.finalized` und
|
||||
`invoice.paid` zusaetzlich abonnieren -- die Webhook-Route ist bereits
|
||||
vorbereitet (`/api/billing/webhook/stripe`). Aktuell genuegt fuer die reine
|
||||
CH-Konformitaet aber `checkout.session.completed`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pro Mandant: Rechnungsadresse erfassen
|
||||
|
||||
Damit die Stripe-Rechnung die korrekte Empfaengeranschrift traegt,
|
||||
muessen die strukturierten Adressfelder am Mandanten befuellt sein.
|
||||
Seit 2026-04-20 (rev 2) sind das **einzelne Felder** -- der frueher
|
||||
verwendete mehrzeilige Freitext wurde wieder gesplittet, damit Stripe
|
||||
sie 1:1 in `customer.address` ablegen kann (Stripe verlangt strukturierte
|
||||
Adressen, kein Freitext).
|
||||
|
||||
| Feld am Mandanten | Pflicht | Stripe-Mapping | Beispiel |
|
||||
|-------------------|---------|----------------|----------|
|
||||
| `invoiceCompanyName` | empfohlen | `customer.name` (Fallback: Mandant-Label) | `Muster Treuhand AG` |
|
||||
| `invoiceContactName` | optional | `customer.shipping.name` (`"<z.H.> (<Firma>)"`) + `invoice_data.custom_fields[z. H.]` + `metadata.contactName` | `Buchhaltung` |
|
||||
| `invoiceEmail` | empfohlen | `customer.email` (Stripe verschickt darauf die Rechnung) | `rechnungen@muster-treuhand.ch` |
|
||||
| `invoiceLine1` | **ja** | `customer.address.line1` | `Bahnhofstrasse 1` |
|
||||
| `invoiceLine2` | optional | `customer.address.line2` | `c/o Buchhaltung` |
|
||||
| `invoicePostalCode` | empfohlen | `customer.address.postal_code` | `8000` |
|
||||
| `invoiceCity` | **ja** | `customer.address.city` | `Zuerich` |
|
||||
| `invoiceState` | optional | `customer.address.state` | `ZH` |
|
||||
| `invoiceCountry` | ja (default `CH`) | `customer.address.country` (ISO-3166 Alpha-2) | `CH` |
|
||||
| `invoiceVatNumber` | bei B2B empfohlen | `customer.tax_id_data` (CHE -> `ch_vat`, LI -> `li_uid`, andere -> `eu_vat`) + `invoice_data.custom_fields[UID-Nr. Empfaenger]` + `metadata.vatNumber` | `CHE-123.456.789 MWST` |
|
||||
|
||||
> **Pflichtminimum fuer eine "echte" Stripe-Customer-Adresse:**
|
||||
> `invoiceLine1` **und** `invoiceCity`. Fehlt eines davon, faellt
|
||||
> `_buildStripeAddress` auf `None` zurueck und Stripe erfragt die Adresse
|
||||
> beim Checkout selbst (`billing_address_collection: required`); die
|
||||
> Rechnung enthaelt dann die vom Endkunden eingegebene Adresse statt der
|
||||
> hinterlegten.
|
||||
|
||||
> **`tax_id_data` ist nur beim Customer-_Create_ wirksam.** Aenderst du
|
||||
> `invoiceVatNumber` an einem Mandanten, dessen Stripe-Customer bereits
|
||||
> existiert, musst du die UID einmalig in Stripe haendisch setzen
|
||||
> (Customers -> Tax IDs) -- die App ruft `tax_ids.create` aktuell nicht
|
||||
> auf einem bestehenden Customer auf, weil das `customer.tax_ids` zur
|
||||
> Vermeidung von Duplikaten erfordern wuerde.
|
||||
|
||||
> **Hinweis Bestandsdaten (vor 2026-04-20):** Die alte JSONB-Spalte
|
||||
> `invoiceAddress` (Freitext oder strukturiertes Dict) wird vom
|
||||
> Schema-Reconciler **nicht** automatisch in die neuen Felder
|
||||
> umgeschrieben. Sie bleibt in der DB liegen, wird aber nicht mehr
|
||||
> gelesen oder geschrieben. Bei Bedarf manuell ein einmaliges
|
||||
> SQL-Update fahren oder die Adresse pro Mandant neu im Form erfassen
|
||||
> (Empfehlung fuer Dev-Umgebungen).
|
||||
|
||||
---
|
||||
|
||||
## 4. Test-Checkliste vor Go-Live
|
||||
|
||||
1. Stripe-Account in **Test-Modus** schalten.
|
||||
2. Mandant `Demo Treuhand` anlegen und alle `invoice*`-Felder befuellen
|
||||
(mindestens `invoiceLine1`, `invoiceCity`, `invoiceCountry`).
|
||||
3. Top-Up 25 CHF ausfuehren (Test-Karte 4242 4242 4242 4242).
|
||||
4. Stripe Dashboard -> Customers -> `Demo Treuhand`:
|
||||
- `name` = `invoiceCompanyName`
|
||||
- `email` = `invoiceEmail`
|
||||
- `address` = strukturiert mit line1/postal_code/city/country
|
||||
- `tax_ids` enthaelt die UID-Nr (Typ `ch_vat`)
|
||||
- `metadata.mandateId` / `metadata.mandateLabel` gesetzt
|
||||
5. Stripe Dashboard -> Invoices -> letzte Rechnung oeffnen:
|
||||
- Status `Paid`
|
||||
- Empfaenger-Block oben links zeigt strukturierte Adresse + UID
|
||||
- `Custom fields` zeigen `UID-Nr. Empfaenger` und ggf. `z. H.`
|
||||
- Zeile `MWST 8.1%` separat ausgewiesen, Total = Netto + MWST
|
||||
- Footer `Diese Rechnung wurde bereits via Kreditkarte bezahlt`
|
||||
- Header zeigt PowerOn-UID
|
||||
6. PDF herunterladen und mit Treuhand abgleichen.
|
||||
|
||||
Wenn alle 6 Punkte stimmen: Live-Modus aktivieren und Roll-out.
|
||||
Loading…
Reference in a new issue