# 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 "` 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 "` (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 2–32) 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 2–32 | 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/`)