# Mandate-Identifier: `name` (Kurzzeichen) und `label` (Voller Name) ## Ueberblick Ein **Mandate** (Mandant / Tenant) hat zwei semantisch getrennte Bezeichner und einen technischen Primaerschluessel: | Feld | Rolle | Aenderbarkeit | Format | | ------- | --------------------------------------------- | -------------------------------------- | ------------------------------- | | `id` | Technischer PK / FK-Anker (UUID) | Niemals (auch nicht durch SysAdmin) | UUID v4 | | `name` | **Kurzzeichen** — global eindeutiger Slug, technischer Identifier in URLs/Logs/Audit | Nur **PlatformAdmin** oder **SysAdmin** | `^[a-z0-9]+(-[a-z0-9]+)*$`, Laenge 2–32 | | `label` | **Voller Name** — Anzeige-Name fuer das UI | **MandateAdmin** und hoeher | non-empty String, beliebige Zeichen | `id` bleibt der einzige FK-Anker. Saemtliche Tabellen (`Role.mandateId`, `UserMandate.mandateId`, `MandateSubscription.mandateId`, `BillingSettings.mandateId`, …) referenzieren `id`, **nicht** `name`. Rebranding (Label-Wechsel) bricht damit niemals Audit-Trails oder Foreign Keys. ## Warum zwei Felder? - **Audit-Stabilitaet:** Audit-Reports und Logs zeigen `name` als kompakten, stabilen Code. Wenn ein Kunde sein Anzeige-Label aendert ("Mueller AG" → "Mueller Holding"), bleibt der Code (`mueller-ag`) gleich. Sonst wuerden historische Reports unleserlich werden. - **URL-/API-Friendliness:** `name` ist URL-tauglich (lowercase, keine Leerzeichen, keine Sonderzeichen). Zukuenftige Subdomain- oder Pfad-Routing-Features koennen `name` direkt verwenden. - **Klare Rollen-Trennung:** MandateAdmin darf sein Label umbenennen, ohne den globalen Identifier zu beeinflussen; nur Plattform-Governance darf den Identifier touchieren. ## Format-Regeln `name` muss erfuellen: - nur `a-z`, `0-9`, `-` - nicht mit `-` beginnen oder enden - keine doppelten Bindestriche - Laenge 2–32 Zeichen `label` muss: - nicht-leer (nach Trim) - darf alle Zeichen enthalten (Umlaute, Akzente, Punkte, Leerzeichen) ## Slug-Generierung (Auto-Allokation) Wenn `name` bei einem POST nicht angegeben oder leer ist, generiert der Server ihn aus `label`: 1. **Transliteration**: `ae | oe | ue | ss` fuer `ä | ö | ü | ß` (auch Grossvarianten). 2. **Lowercasen** und alles, was nicht `[a-z0-9]` ist, zu `-` ersetzen. 3. **Bindestriche kollabieren** und an Raendern trimmen. 4. **Min-Laenge sichern** (Auffuellen mit `x`). 5. **Max-Laenge erzwingen** (an letztem Bindestrich ueberhalb der Laengenschranke abschneiden). 6. **Kollisions-Suffix** `-2`, `-3`, … wenn der Basis-Slug bereits existiert. Beispiele: | Label | Erzeugter `name` | | ------------------------- | ------------------------ | | `Müller AG` | `mueller-ag` | | `Home Patrick.Möller` | `home-patrick-moeller` | | `Müller AG` (zweites Mal) | `mueller-ag-2` | | ` ` (leer) | `mn` (Fallback) | Code: `gateway/modules/shared/mandateNameUtils.py` (Backend) und `frontend_nyla/src/utils/slugUtils.ts` + `mandateNameUtils.ts` (Frontend, mirror). ## RBAC und Editierbarkeit | Operation | Rolle | Verhalten | | ------------------------- | -------------------------------------- | ------------------------------------------------- | | `POST /api/mandates/` | PlatformAdmin | `label` mandatory; `name` optional → Server generiert | | `PUT /api/mandates/{id}` `name` | PlatformAdmin oder SysAdmin | Format + Uniqueness validiert (excl. self) | | `PUT /api/mandates/{id}` `name` | MandateAdmin | **Ignoriert** (Whitelist `_MANDATE_ADMIN_EDITABLE_FIELDS = {"label"}`) | | `PUT /api/mandates/{id}` `label` | MandateAdmin oder hoeher | Pflicht: nicht-leer (nach Trim) | | `PUT /api/mandates/{id}` `isSystem` | SysAdmin | Nur SysAdmin | | `DELETE /api/mandates/{id}?force=true` | PlatformAdmin (X-Confirm-Name=`name`) | Hard-Delete mit Cascade | Im UI ist das Kurzzeichen-Feld in den Mandate-Formularen vom Typ `slug` (Live-Maskierung, Auto-Vorschlag aus `label` im Create-Modus, im Edit fuer non-PlatformAdmin disabled). ## Bootstrap-Migration `interfaceBootstrap._migrateMandateNameLabelSlugRules()` laeuft idempotent beim Boot und bringt Bestandsdaten auf das neue Schema: 1. Wenn `label` leer/None → `label := name`. 2. Wenn `name` nicht regex-konform → Slug aus `label` generieren mit Kollisions-Suffix. 3. Stable order ueber `id`-String, deterministisch. 4. Zweiter Lauf ist No-op. Tests: `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`. ## UI-Anzeige (Konvention) - Listen / Auswahlen / Header → `mandateDisplayLabel(m)` = `label || name || id` (Frontend-Util `mandateDisplayUtils.ts`). - Detail-Pages mit Identifier-Bezug → `mandateDisplayLineLabelThenSlug(m)` = `Voller Name (kurzzeichen)` wenn beide vorhanden und unterschiedlich. - Hard-Delete-Confirm → fragt explizit nach `name` (Kurzzeichen) ab. ## Verwandte Dateien - Backend: `gateway/modules/datamodels/datamodelUam.py` (Pydantic-Modell), `gateway/modules/interfaces/interfaceDbApp.py` (`createMandate`, `updateMandate`, `_provisionMandateForUser`, `_generateUniqueMandateName`), `gateway/modules/routes/routeDataMandates.py`, `gateway/modules/shared/mandateNameUtils.py`. - Frontend: `frontend_nyla/src/types/mandate.ts`, `frontend_nyla/src/api/mandateApi.ts`, `frontend_nyla/src/utils/slugUtils.ts`, `frontend_nyla/src/utils/mandateNameUtils.ts`, `frontend_nyla/src/utils/mandateDisplayUtils.ts`, `frontend_nyla/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` (slug-Type Renderer). - Tests: `gateway/tests/unit/shared/test_mandateNameUtils.py`, `gateway/tests/unit/bootstrap/test_mandateNameMigration.py`, `gateway/tests/integration/mandates/`. - Plan-Doku: `wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md`.