wiki/c-work/4-done/2026-04-mandate-name-label-logic.md

184 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- started: 2026-04-18 -->
<!-- completed: 2026-04-18 -->
<!-- component: gateway, frontend-nyla, platform -->
# Mandate `name` (Kurzzeichen) und `label` (Voller Name) Logik
## Beschreibung und Kontext
Heute existieren am `Mandate`-Modell die Felder `name` und `label` ohne klare Trennung der Verantwortlichkeiten:
- `name` ist als `str` required, ohne Format-Validierung, ohne Unique-Garantie auf Modell-Ebene, ohne Immutability.
- `label` ist `Optional[str]`, kann leer sein.
- Beim Auto-Provisioning (Home-Mandant, Onboarding) wird `name == label == "Home <username>"` gesetzt — das verstösst gegen jede sinnvolle Slug-Konvention und macht `name` als URL-/Audit-Identifier unbrauchbar.
- In der UI wird inkonsistent mal `m.label || m.name`, mal `mandate.name`, mal sogar `name (label)` angezeigt.
**Business-Treiber:**
- **Audit-Stabilität:** Audit-Trails referenzieren Mandate per `name`. Wenn der Anzeige-Name (Label) frei änderbar ist und gleichzeitig als technischer Code dient, brechen Audit-Reports beim Rebranding.
- **URL-/API-Friendliness:** Der `name` soll als technischer Code in URLs, Logs, und systemexternen Referenzen taugen.
- **Klare Rollen-Trennung:** Mandate-Admin darf sein Label umbenennen ohne den globalen Identifier zu touchieren; nur Plattform-Governance (PlatformAdmin) darf den Identifier ändern.
**Risiko bei Nicht-Umsetzung:** Inkonsistente Anzeige im UI, Verwechslungs-Risiko, brüchige Audit-Trails, keine sauberen Slugs/URLs.
## Fokus und kritische Details
- **Migration ist heikel:** existierende Mandate haben `name`-Werte mit Spaces/Sonderzeichen (z.B. `"Home patrick"`). Migration muss deterministisch + idempotent sein, alle FK-Verweise (`Role.mandateId`, `UserMandate.mandateId`, `MandateSubscription.mandateId`, `BillingSettings.mandateId`, ...) bleiben unverändert (FK ist `id`, nicht `name`).
- **Auto-Provisioning** (`_provisionMandateForUser`, `routeSecurityLocal.py` Z. 213-219, 467-477, 884-894) setzt heute `name == label`. Muss umgebaut werden: `label = "Home <username>"` (oder `companyName`), `name = _generateUniqueMandateName(label)`.
- **PUT-Route** unterscheidet heute zwischen PlatformAdmin (alles editierbar) und MandateAdmin (`{"label"}`-Whitelist). Neu: PlatformAdmin behält `name`-Recht, MandateAdmin nur `label` — Status quo passt, aber Server muss `name`-Format validieren bei Update.
- **`isSystem`-Mandate:** Root-Mandant darf nicht versehentlich umbenannt werden — bestehende Sicherung greift bereits über `frontend_readonly` und das Warning-Banner.
- **UI-Stellen mit `mandate.name` als technischer Identifier** (Delete-Confirm, X-Confirm-Name) bleiben semantisch korrekt — `name` IST hier der technische Identifier und das ist gewollt.
- **i18n:** Die Label-Texte ("Kurzzeichen", "Voller Name") müssen via `t()` übersetzbar bleiben; kein hartkodiertes Deutsch in Frontend-Komponenten.
- **TypeScript-Typen:** `frontend_nyla/src/types/mandate.ts` und `useMandates`-Hook müssen mitgepflegt werden.
## Ziel und Nicht-Ziele
**Ziel:**
- Klare semantische Trennung: `name` = Unique-Code (Slug), `label` = Anzeige-Name. Beide mandatory.
- Server-seitige Validierung von `name` (Regex `^[a-z0-9]+(-[a-z0-9]+)*$`, Länge 232) bei Create und Update.
- Slug-Auto-Generation aus `label` mit Transliteration (ä→ae, ö→oe, ü→ue, ß→ss) und Unique-Suffix-Logik (`-2`, `-3`, ...).
- Live-strict-Input-Validierung im Frontend für `name`-Feld.
- Konsistente UI-Anzeige: überall `label` rendern (`name` nur als Fallback wenn label fehlt — Migration sollte das ausschliessen, aber Defensive Coding bleibt).
- Einmalige Migration: `label := name` wenn Label leer; `name` zu gültigem Slug normalisieren wenn nötig (mit Unique-Suffix).
- PUT-Route: nur PlatformAdmin darf `name` ändern (Status quo); MandateAdmin nur `label`.
**Explizit NICHT:**
- Keine Änderung der DB-FK-Struktur (Mandate.id bleibt UUID-PK; FK-Verweise referenzieren weiterhin `id`, nicht `name`).
- Kein Rename von Tabellenspalten.
- Keine Auswirkung auf Audit-Tabellen (die loggen weiterhin `mandateId` als UUID).
- Keine API-Versionierung; Breaking Change wird hingenommen, da nur PlatformAdmin direkt `name` setzt.
- Kein Slug-Subdomain-Routing (zukünftiges Feature; nur Vorbereitung).
## Betroffene Module
- **Gateway:**
- `gateway/modules/datamodels/datamodelUam.py` (`Mandate` Pydantic-Klasse)
- `gateway/modules/interfaces/interfaceDbApp.py` (`createMandate`, `_provisionMandateForUser`, `updateMandate`, NEU: `_generateUniqueMandateName`)
- `gateway/modules/routes/routeDataMandates.py` (POST/PUT-Validierung)
- `gateway/modules/routes/routeSecurityLocal.py` (3 Aufrufstellen `_provisionMandateForUser`)
- `gateway/modules/interfaces/interfaceBootstrap.py` (Migrations-Hook beim Boot)
- NEU: `gateway/modules/shared/mandateNameUtils.py` (Slug-Helpers, Validierung, Transliteration)
- **Frontend:**
- `frontend_nyla/src/types/mandate.ts`
- `frontend_nyla/src/hooks/useMandates.ts`
- `frontend_nyla/src/pages/admin/AdminMandatesPage.tsx` (Modal-Texte, Subtitle)
- `frontend_nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx` (Anzeige `name (label)``label (name)`)
- `frontend_nyla/src/components/FormGenerator/...` (Live-strict-Validierung für Slug-Type, falls neuer `frontend_type: "slug"` eingeführt)
- Display-Audit aller `m.label || m.name`-Stellen (Sicherstellen, dass Reihenfolge korrekt ist)
- **DB-Migration:** ja (idempotent in Bootstrap, kein Alembic — passt zum Projekt-Pattern)
- **Andere Komponenten:** keine
## Entscheidungen
| Datum | Entscheidung | Begründung |
|-------|-------------|------------|
| 2026-04-18 | Beide Felder (`name`, `label`) mandatory, non-empty | User-Anforderung; Migration füllt fehlende Werte |
| 2026-04-18 | Slug-Regex `^[a-z0-9]+(-[a-z0-9]+)*$`, Länge 232 | Kompakt, URL/Subdomain-tauglich |
| 2026-04-18 | Transliteration ä/ö/ü/ß → ae/oe/ue/ss; Rest non-alnum → `-` | Deutsche Mandate-Namen häufig; Lesbarkeit |
| 2026-04-18 | Unique-Suffix-Strategie: `mein-mandant`, `mein-mandant-2`, `mein-mandant-3`, ... | Einfach, deterministisch, lesbar |
| 2026-04-18 | `name`-Editing: weiterhin nur PlatformAdmin (Status quo); MandateAdmin nur `label` | Bestehende `_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}` passt |
| 2026-04-18 | UI-Form-Validierung: live-strict (Eingabe blockiert ungültige Zeichen) | UX-klarer als nachträglicher Fehler |
| 2026-04-18 | Migration: idempotent in `interfaceBootstrap`, kein Alembic | Passt zu bestehendem Bootstrap-Pattern |
| 2026-04-18 | Migration `label := name` wenn label leer; `name`-Slug-Normalisierung mit Unique-Suffix | User-Anforderung |
| 2026-04-18 | Pydantic-`field_validator` für `name`-Format; Unique-Check ausserhalb (in `createMandate`/`updateMandate`) | Pydantic kann nicht DB-uniqueness prüfen |
| 2026-04-18 | json_schema_extra labels: `name` → "Kurzzeichen", `label` → "Voller Name" | User-Anforderung |
| 2026-04-18 | Neuer `frontend_type: "slug"` mit Live-Maskierung im FormGenerator | Wiederverwendbar für andere Slug-Felder zukünftig |
## Umsetzungs-Checkliste
### Phase 1 — Shared Utilities
- [x] `gateway/modules/shared/mandateNameUtils.py` mit Transliteration, Slugify, Validierung
- [x] Tests: `gateway/tests/unit/shared/test_mandateNameUtils.py`
### Phase 2 — Pydantic-Modell
- [x] `Mandate.name`: json_schema_extra `Kurzzeichen`, `frontend_type: "slug"`, `pattern`, `min_length`/`max_length`
- [x] `Mandate.label`: required, json_schema_extra `Voller Name`, `frontend_required`
- [x] `field_validator('name')` und `field_validator('label')`
### Phase 3 — Interface (Backend-Logik)
- [x] `interfaceDbApp._generateUniqueMandateName(label, excludeId)` mit Suffix-Schleife
- [x] `createMandate(name=None, label, enabled)`: Auto-Generation, Format/Uniqueness, label-mandatory
- [x] `updateMandate(mandateId, data)`: Format + Uniqueness (excluding self), label-nonempty
- [x] `_provisionMandateForUser(userId, mandateLabel, planKey)`: Parameter umbenannt, Slug abgeleitet
### Phase 4 — Routes
- [x] `POST /api/mandates/`: name optional, server-generiert
- [x] `PUT /api/mandates/{id}`: Format-Check fuer name (PlatformAdmin), label-nonempty
- [x] `routeSecurityLocal.py`: 3 Aufrufstellen auf `mandateLabel` umgestellt
### Phase 5 — Migration in Bootstrap
- [x] `_migrateMandateNameLabelSlugRules` idempotent in `initBootstrap`
- [x] Tests: `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`
### Phase 6 — Frontend Modell + Hook
- [x] `types/mandate.ts`: `label: string` (mandatory) + Doc
- [x] `api/mandateApi.ts`: `MandateCreateData` + Semantik-Doc
- [x] `hooks/useMandates.ts`: label-zuerst Sortierung, Validierung in Create/Update
### Phase 7 — FormGenerator: `slug`-Type
- [x] `slug` in `attributeTypeMapper.ts` (mapped auf `text`)
- [x] `FormGeneratorForm` Slug-Render: Live-Masking, Hint, Validierung, Auto-Vorschlag aus konfigurierbarer `slugSource` (default `label`)
- [x] **Korrektur**: generische Logik in `frontend_nyla/src/utils/slugUtils.ts` ausgelagert; `mandateNameUtils.ts` ist dünner Wrapper. FormGenerator bleibt domain-agnostisch.
### Phase 8 — UI Display Konsistenz
- [x] `mandateDisplayUtils.ts` (`mandateDisplayLabel`, `mandateDisplayLineLabelThenSlug`)
- [x] `AdminUserAccessOverviewPage.tsx`: `label (name)` Format
- [x] `AdminMandatesPage.tsx`: Subtitle-Hinweis + isSystem-Warnung + Delete-Confirm fragt nach Kurzzeichen
- [x] Audit aller `m.label || m.name`-Stellen: 9 Frontend-Dateien auf `mandateDisplayLabel` umgestellt
### Phase 9 — Tests
- [x] Unit: `mandateNameUtils` (7 Tests)
- [x] Unit: Migration-Idempotenz (9 Tests)
- [x] Integration: `createMandate` (12 Tests) — auto-name, validation, collision, RBAC, label-mandatory
- [x] Integration: `updateMandate` (12 Tests) — RBAC name (PlatformAdmin/SysAdmin/MandateAdmin), validation, collision, protected fields
- [x] Integration: `_provisionMandateForUser` Slug-Contract (9 Tests) — Umlaute, Kollisionen, label-mandatory, plan-guard
- **Total: 49 Tests, alle gruen.**
### Phase 10 — Dokumentation
- [x] Neue Datei `b-reference/platform/mandate.md` (kanonische Referenz)
- [x] `TOPICS.md` Eintrag fuer Mandate-Identifier
- [x] Diese Plan-Doku abgeschlossen, Status `done`
## Akzeptanzkriterien
| # | Kriterium (Given-When-Then) | Prio |
|---|---------------------------|------|
| 1 | Given ein PlatformAdmin im Mandate-Create-Form, When er ein `Voller Name` mit Umlauten ("Müller AG") eingibt und kein `Kurzzeichen` setzt, Then generiert der Server `mueller-ag` und persistiert beide Felder | must |
| 2 | Given ein bereits existierender Mandant mit `name == "mueller-ag"`, When ein zweiter mit Label "Müller AG" angelegt wird ohne explizites Kurzzeichen, Then erhält dieser `name == "mueller-ag-2"` | must |
| 3 | Given ein Mandate-Admin (kein PlatformAdmin), When er versucht das `name`-Feld via PUT zu ändern, Then ignoriert der Server das Feld (Whitelist) und gibt 200 mit nur `label`-Update zurück | must |
| 4 | Given ein PlatformAdmin, When er einen Mandate-Namen mit ungültigen Zeichen ("ABC Müller!") via PUT setzt, Then antwortet die API mit 400 und einer i18n-Fehlermeldung | must |
| 5 | Given das System bootet, When der Migrations-Hook läuft auf einem Mandanten ohne `label` und mit `name == "Home patrick"`, Then setzt er `label := "Home patrick"` und `name := "home-patrick"` (mit Suffix wenn Kollision) | must |
| 6 | Given die Migration läuft zweimal hintereinander, When der zweite Lauf prüft, Then ändert er nichts (Idempotenz) | must |
| 7 | Given ein User im Mandate-Create-Form, When er versucht ins `Kurzzeichen`-Feld Grossbuchstaben oder Spaces einzutippen, Then werden diese Zeichen vom Input live verworfen | must |
| 8 | Given ein PlatformAdmin im Edit-Modal eines `isSystem`-Mandanten, When er die Felder sieht, Then ist `Kurzzeichen` disabled und ein Warn-Banner erklärt warum | must |
| 9 | Given die Mandate-Liste in `AdminUserAccessOverviewPage`, When ein Mandant gerendert wird, Then steht das Label gross/primary und das `name` (Kurzzeichen) sekundär in Klammern | should |
| 10 | Given das Auto-Provisioning bei Registrierung eines Users `Patrick.Möller`, When es läuft, Then ist `label == "Home Patrick.Möller"` und `name == "home-patrick-moeller"` (oder Suffix) | must |
## Testplan
| ID | AC | Art | Automatisiert | Repo-Pfad | Status |
|----|----|-----|---------------|-----------|--------|
| T1 | -- | unit | ja | `gateway/tests/unit/shared/test_mandateNameUtils.py` | done (7) |
| T2 | 1, 2 | integration | ja | `gateway/tests/integration/mandates/test_createMandate.py` | done (12) |
| T3 | 3, 4 | integration | ja | `gateway/tests/integration/mandates/test_updateMandate.py` | done (12) |
| T4 | 5, 6 | integration | ja | `gateway/tests/unit/bootstrap/test_mandateNameMigration.py` | done (9) |
| T5 | 7, 8 | manual | nein | Frontend Smoke-Test AdminMandatesPage | done |
| T6 | 9 | manual | nein | Frontend Smoke-Test AdminUserAccessOverviewPage | done |
| T7 | 10 | integration | ja | `gateway/tests/integration/mandates/test_provisionMandate.py` | done (9) |
| T8 | -- | covered by T2/T3 | -- | -- | n/a (Pydantic-Validators implizit getestet) |
## Links
- PR: --
- Issue: --
- Wiki RBAC-Kontext: `wiki/b-reference/platform/rbac.md`
- Bezogene UI-Konsolidierung: `wiki/c-work/1-plan/2026-04-pm-consolidated-customer-requirements.md`
## Abschluss
- [x] `b-reference/platform/mandate.md` neu angelegt (kanonische Referenz Mandate-Identifier)
- [x] `TOPICS.md` aktualisiert (Eintrag "Mandate-Identifier")
- [x] Dieses Dokument → `wiki/c-work/4-done/` verschoben (Repo-Konvention statt `z-archive/`)