13 KiB
13 KiB
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:
nameist alsstrrequired, ohne Format-Validierung, ohne Unique-Garantie auf Modell-Ebene, ohne Immutability.labelistOptional[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 machtnameals URL-/Audit-Identifier unbrauchbar. - In der UI wird inkonsistent mal
m.label || m.name, malmandate.name, mal sogarname (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
namesoll 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 istid, nichtname). - Auto-Provisioning (
_provisionMandateForUser,routeSecurityLocal.pyZ. 213-219, 467-477, 884-894) setzt heutename == label. Muss umgebaut werden:label = "Home <username>"(odercompanyName),name = _generateUniqueMandateName(label). - PUT-Route unterscheidet heute zwischen PlatformAdmin (alles editierbar) und MandateAdmin (
{"label"}-Whitelist). Neu: PlatformAdmin behältname-Recht, MandateAdmin nurlabel— Status quo passt, aber Server mussname-Format validieren bei Update. isSystem-Mandate: Root-Mandant darf nicht versehentlich umbenannt werden — bestehende Sicherung greift bereits überfrontend_readonlyund das Warning-Banner.- UI-Stellen mit
mandate.nameals technischer Identifier (Delete-Confirm, X-Confirm-Name) bleiben semantisch korrekt —nameIST 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:
ui-nyla/src/types/mandate.tsunduseMandates-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
labelmit 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
labelrendern (namenur als Fallback wenn label fehlt — Migration sollte das ausschliessen, aber Defensive Coding bleibt). - Einmalige Migration:
label := namewenn Label leer;namezu gültigem Slug normalisieren wenn nötig (mit Unique-Suffix). - PUT-Route: nur PlatformAdmin darf
nameändern (Status quo); MandateAdmin nurlabel.
Explizit NICHT:
- Keine Änderung der DB-FK-Struktur (Mandate.id bleibt UUID-PK; FK-Verweise referenzieren weiterhin
id, nichtname). - Kein Rename von Tabellenspalten.
- Keine Auswirkung auf Audit-Tabellen (die loggen weiterhin
mandateIdals UUID). - Keine API-Versionierung; Breaking Change wird hingenommen, da nur PlatformAdmin direkt
namesetzt. - Kein Slug-Subdomain-Routing (zukünftiges Feature; nur Vorbereitung).
Betroffene Module
- Gateway:
platform-core/modules/datamodels/datamodelUam.py(MandatePydantic-Klasse)platform-core/modules/interfaces/interfaceDbApp.py(createMandate,_provisionMandateForUser,updateMandate, NEU:_generateUniqueMandateName)platform-core/modules/routes/routeDataMandates.py(POST/PUT-Validierung)platform-core/modules/routes/routeSecurityLocal.py(3 Aufrufstellen_provisionMandateForUser)platform-core/modules/interfaces/interfaceBootstrap.py(Migrations-Hook beim Boot)- NEU:
platform-core/modules/shared/mandateNameUtils.py(Slug-Helpers, Validierung, Transliteration)
- Frontend:
ui-nyla/src/types/mandate.tsui-nyla/src/hooks/useMandates.tsui-nyla/src/pages/admin/AdminMandatesPage.tsx(Modal-Texte, Subtitle)ui-nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx(Anzeigename (label)→label (name))ui-nyla/src/components/FormGenerator/...(Live-strict-Validierung für Slug-Type, falls neuerfrontend_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
platform-core/modules/shared/mandateNameUtils.pymit Transliteration, Slugify, Validierung- Tests:
platform-core/tests/unit/shared/test_mandateNameUtils.py
Phase 2 — Pydantic-Modell
Mandate.name: json_schema_extraKurzzeichen,frontend_type: "slug",pattern,min_length/max_lengthMandate.label: required, json_schema_extraVoller Name,frontend_requiredfield_validator('name')undfield_validator('label')
Phase 3 — Interface (Backend-Logik)
interfaceDbApp._generateUniqueMandateName(label, excludeId)mit Suffix-SchleifecreateMandate(name=None, label, enabled): Auto-Generation, Format/Uniqueness, label-mandatoryupdateMandate(mandateId, data): Format + Uniqueness (excluding self), label-nonempty_provisionMandateForUser(userId, mandateLabel, planKey): Parameter umbenannt, Slug abgeleitet
Phase 4 — Routes
POST /api/mandates/: name optional, server-generiertPUT /api/mandates/{id}: Format-Check fuer name (PlatformAdmin), label-nonemptyrouteSecurityLocal.py: 3 Aufrufstellen aufmandateLabelumgestellt
Phase 5 — Migration in Bootstrap
_migrateMandateNameLabelSlugRulesidempotent ininitBootstrap- Tests:
platform-core/tests/unit/bootstrap/test_mandateNameMigration.py
Phase 6 — Frontend Modell + Hook
types/mandate.ts:label: string(mandatory) + Docapi/mandateApi.ts:MandateCreateData+ Semantik-Dochooks/useMandates.ts: label-zuerst Sortierung, Validierung in Create/Update
Phase 7 — FormGenerator: slug-Type
sluginattributeTypeMapper.ts(mapped auftext)FormGeneratorFormSlug-Render: Live-Masking, Hint, Validierung, Auto-Vorschlag aus konfigurierbarerslugSource(defaultlabel)- Korrektur: generische Logik in
ui-nyla/src/utils/slugUtils.tsausgelagert;mandateNameUtils.tsist dünner Wrapper. FormGenerator bleibt domain-agnostisch.
Phase 8 — UI Display Konsistenz
mandateDisplayUtils.ts(mandateDisplayLabel,mandateDisplayLineLabelThenSlug)AdminUserAccessOverviewPage.tsx:label (name)FormatAdminMandatesPage.tsx: Subtitle-Hinweis + isSystem-Warnung + Delete-Confirm fragt nach Kurzzeichen- Audit aller
m.label || m.name-Stellen: 9 Frontend-Dateien aufmandateDisplayLabelumgestellt
Phase 9 — Tests
- Unit:
mandateNameUtils(7 Tests) - Unit: Migration-Idempotenz (9 Tests)
- Integration:
createMandate(12 Tests) — auto-name, validation, collision, RBAC, label-mandatory - Integration:
updateMandate(12 Tests) — RBAC name (PlatformAdmin/SysAdmin/MandateAdmin), validation, collision, protected fields - Integration:
_provisionMandateForUserSlug-Contract (9 Tests) — Umlaute, Kollisionen, label-mandatory, plan-guard - Total: 49 Tests, alle gruen.
Phase 10 — Dokumentation
- Neue Datei
b-reference/platform/mandate.md(kanonische Referenz) TOPICS.mdEintrag fuer Mandate-Identifier- 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 | platform-core/tests/unit/shared/test_mandateNameUtils.py |
done (7) |
| T2 | 1, 2 | integration | ja | platform-core/tests/integration/mandates/test_createMandate.py |
done (12) |
| T3 | 3, 4 | integration | ja | platform-core/tests/integration/mandates/test_updateMandate.py |
done (12) |
| T4 | 5, 6 | integration | ja | platform-core/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 | platform-core/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
b-reference/platform/mandate.mdneu angelegt (kanonische Referenz Mandate-Identifier)TOPICS.mdaktualisiert (Eintrag "Mandate-Identifier")- Dieses Dokument →
wiki/c-work/4-done/verschoben (Repo-Konvention stattz-archive/)