wiki/b-reference/platform/mandate.md
2026-06-02 09:42:12 +02:00

96 lines
6.2 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: canonical -->
<!-- lastReviewed: 2026-04-18 -->
<!-- verifiedAgainst: gateway + ui-nyla (codebase audit 2026-04-18) -->
# 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 232 |
| `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 232 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: `platform-core/modules/shared/mandateNameUtils.py` (Backend) und `ui-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: `platform-core/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: `platform-core/modules/datamodels/datamodelUam.py` (Pydantic-Modell), `platform-core/modules/interfaces/interfaceDbApp.py` (`createMandate`, `updateMandate`, `_provisionMandateForUser`, `_generateUniqueMandateName`), `platform-core/modules/routes/routeDataMandates.py`, `platform-core/modules/shared/mandateNameUtils.py`.
- Frontend: `ui-nyla/src/types/mandate.ts`, `ui-nyla/src/api/mandateApi.ts`, `ui-nyla/src/utils/slugUtils.ts`, `ui-nyla/src/utils/mandateNameUtils.ts`, `ui-nyla/src/utils/mandateDisplayUtils.ts`, `ui-nyla/src/components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` (slug-Type Renderer).
- Tests: `platform-core/tests/unit/shared/test_mandateNameUtils.py`, `platform-core/tests/unit/bootstrap/test_mandateNameMigration.py`, `platform-core/tests/integration/mandates/`.
- Plan-Doku: `wiki/c-work/1-plan/2026-04-mandate-name-label-logic.md`.