"""Résolution des rôles du pipeline vente depuis le contrat RBAC 50 rôles. Réutilisation (workflow #5 : zéro duplication) de l'unique source de vérité des rôles ERPNext : `05_deliverables_mvp/rbac/rbac_50_roles.json` (validé par `rbac.schema.json` dans le job CI `rbac-tests`). Le contrat pipeline (`workflow_vente_spec.json`) ne cite JAMAIS un nom de rôle Frappe en dur : il référence l'`id` stable d'un rôle RBAC → ici on résout `id → erpnext_role_name`. Conséquence anti-invention (#6) : un `role_id` absent du contrat RBAC lève une erreur (aucun rôle fabriqué), et renommer un rôle côté RBAC se propage automatiquement au workflow sans édition manuelle. """ from __future__ import annotations import json import os _HERE = os.path.dirname(os.path.abspath(__file__)) _DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", "..")) RBAC_CONTRACT_PATH = os.path.join(_DELIVERABLES, "rbac", "rbac_50_roles.json") class RoleResolver: """Table `role_id → erpnext_role_name` construite depuis le contrat RBAC.""" def __init__(self, contract: dict) -> None: self._by_id: dict[str, dict] = {} for role in contract.get("roles", []): rid = role.get("id") if rid is None: raise ValueError("Rôle RBAC sans `id` — contrat corrompu.") if rid in self._by_id: raise ValueError(f"`id` de rôle RBAC dupliqué : {rid!r}") self._by_id[rid] = role self._portails = set(contract.get("portails_business", [])) @classmethod def from_path(cls, path: str = RBAC_CONTRACT_PATH) -> "RoleResolver": with open(path, encoding="utf-8") as fh: return cls(json.load(fh)) def erpnext_name(self, role_id: str) -> str: """`id` RBAC → nom de rôle Frappe. Lève si l'`id` n'existe pas.""" role = self._by_id.get(role_id) if role is None: raise KeyError( f"role_id {role_id!r} introuvable dans rbac_50_roles.json " f"(aucun rôle inventé · #6)." ) name = role.get("erpnext_role_name") if not name: raise ValueError(f"Rôle {role_id!r} sans `erpnext_role_name`.") return name def portail(self, role_id: str) -> str | None: role = self._by_id.get(role_id) return role.get("portail") if role else None def known_ids(self) -> frozenset[str]: return frozenset(self._by_id)