"""Construction des `Role Profile` depuis `rbac_50_roles.json`. Entrée : le contrat RBAC 50 rôles (déjà validé par `rbac.schema.json` dans le job CI `rbac-tests`). Sortie : un « bundle » déterministe : - role_profile → 1 fixture `Role Profile` par portail distinct du contrat, chacun bundlant les `erpnext_role_name` de ses rôles. - manifest → traçabilité (comptes, portails métier vs technique, mapping portail → profil), 100 % dérivé du contrat (contrainte #6). Le contrat porte 6 portails distincts (5 métier listés dans `portails_business` + la console technique `plateforme`). Chaque rôle a EXACTEMENT un `portail` → la couverture des 50 rôles par les profils est bijective (invariant vérifié par le CLI). La source de vérité des comptes est le JSON, pas la doc (SPEC §3). Déterminisme : profils triés par clé de portail, rôles triés par nom Frappe → sortie reproductible bit-à-bit à contrat constant (diffable, re-générable en CI sans horodatage volatil). """ from __future__ import annotations from typing import Any from . import frappe def _roles_by_portail(contract: dict) -> dict[str, list[str]]: """Regroupe les `erpnext_role_name` par `portail` (ordre trié, stable).""" grouped: dict[str, list[str]] = {} for r in contract["roles"]: portail = r["portail"] name = r["erpnext_role_name"] grouped.setdefault(portail, []).append(name) for portail in grouped: names = grouped[portail] if len(names) != len(set(names)): raise ValueError(f"Rôle dupliqué dans le portail {portail!r}.") names.sort() return grouped def build_bundle(contract: dict) -> dict[str, Any]: """Transforme le contrat RBAC en bundle de `Role Profile` déterministe.""" grouped = _roles_by_portail(contract) portails_metier = set(contract.get("portails_business", [])) role_profiles: list[dict] = [] profiles_meta: list[dict] = [] for portail in sorted(grouped): role_names = grouped[portail] role_profiles.append(frappe.role_profile_fixture(portail, role_names)) profiles_meta.append( { "portail": portail, "role_profile": frappe.role_profile_name(portail), "metier": portail in portails_metier, "nb_roles": len(role_names), } ) nb_metier = sum(1 for p in profiles_meta if p["metier"]) manifest = { "generated_from": "rbac_50_roles.json", "source_version": contract["version"], "cible_rbac_roles": contract["cible_rbac_roles"], "counts": { "role_profiles": len(role_profiles), "portails_metier": nb_metier, "portails_techniques": len(role_profiles) - nb_metier, "roles_couverts": sum(p["nb_roles"] for p in profiles_meta), }, # Mapping portail → profil (métier vs console technique `plateforme`), # pour que l'agent ERPNext assigne le bon profil à chaque utilisateur. "profiles": profiles_meta, } return { "manifest": manifest, "role_profile": role_profiles, }