wiki/c-work/4-done/2026-04-mandate-name-label-logic.md
2026-06-02 09:42:12 +02:00

13 KiB
Raw Blame History

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: ui-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:
    • platform-core/modules/datamodels/datamodelUam.py (Mandate Pydantic-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.ts
    • ui-nyla/src/hooks/useMandates.ts
    • ui-nyla/src/pages/admin/AdminMandatesPage.tsx (Modal-Texte, Subtitle)
    • ui-nyla/src/pages/admin/AdminUserAccessOverviewPage.tsx (Anzeige name (label)label (name))
    • ui-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

  • platform-core/modules/shared/mandateNameUtils.py mit Transliteration, Slugify, Validierung
  • Tests: platform-core/tests/unit/shared/test_mandateNameUtils.py

Phase 2 — Pydantic-Modell

  • Mandate.name: json_schema_extra Kurzzeichen, frontend_type: "slug", pattern, min_length/max_length
  • Mandate.label: required, json_schema_extra Voller Name, frontend_required
  • field_validator('name') und field_validator('label')

Phase 3 — Interface (Backend-Logik)

  • interfaceDbApp._generateUniqueMandateName(label, excludeId) mit Suffix-Schleife
  • createMandate(name=None, label, enabled): Auto-Generation, Format/Uniqueness, label-mandatory
  • updateMandate(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-generiert
  • PUT /api/mandates/{id}: Format-Check fuer name (PlatformAdmin), label-nonempty
  • routeSecurityLocal.py: 3 Aufrufstellen auf mandateLabel umgestellt

Phase 5 — Migration in Bootstrap

  • _migrateMandateNameLabelSlugRules idempotent in initBootstrap
  • Tests: platform-core/tests/unit/bootstrap/test_mandateNameMigration.py

Phase 6 — Frontend Modell + Hook

  • types/mandate.ts: label: string (mandatory) + Doc
  • api/mandateApi.ts: MandateCreateData + Semantik-Doc
  • hooks/useMandates.ts: label-zuerst Sortierung, Validierung in Create/Update

Phase 7 — FormGenerator: slug-Type

  • slug in attributeTypeMapper.ts (mapped auf text)
  • FormGeneratorForm Slug-Render: Live-Masking, Hint, Validierung, Auto-Vorschlag aus konfigurierbarer slugSource (default label)
  • Korrektur: generische Logik in ui-nyla/src/utils/slugUtils.ts ausgelagert; mandateNameUtils.ts ist dünner Wrapper. FormGenerator bleibt domain-agnostisch.

Phase 8 — UI Display Konsistenz

  • mandateDisplayUtils.ts (mandateDisplayLabel, mandateDisplayLineLabelThenSlug)
  • AdminUserAccessOverviewPage.tsx: label (name) Format
  • AdminMandatesPage.tsx: Subtitle-Hinweis + isSystem-Warnung + Delete-Confirm fragt nach Kurzzeichen
  • Audit aller m.label || m.name-Stellen: 9 Frontend-Dateien auf mandateDisplayLabel umgestellt

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: _provisionMandateForUser Slug-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.md Eintrag 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)
  • 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.md neu angelegt (kanonische Referenz Mandate-Identifier)
  • TOPICS.md aktualisiert (Eintrag "Mandate-Identifier")
  • Dieses Dokument → wiki/c-work/4-done/ verschoben (Repo-Konvention statt z-archive/)