"""Assemblage du plan de commissions depuis `bareme_spec.json`. Entrée : le contrat barème (évènements référençant des `update_value` du workflow, des `base_field` du DocType Dossier Vente et des `role_id` RBAC) + un `RoleResolver` (id → nom Frappe issu de rbac_50_roles.json). Sortie : un bundle déterministe (tri stable, aucun horodatage) → reproductible bit-à-bit à contrat constant, donc diffable et re-générable en CI. Contrainte #6 (zéro invention) : aucun taux n'est ajouté — le barème livré porte `taux_pct: null`. Le builder n'ajoute que la résolution des noms de rôle et un manifeste de traçabilité (compte des taux restant à confirmer inclus). """ from __future__ import annotations import os import sys from typing import Any # Réutilisation (workflow #5) du module CRM voisin : le résolveur de rôles # (rbac_50_roles.json) est importé, jamais redéfini ici. _HERE = os.path.dirname(os.path.abspath(__file__)) _CRM = os.path.normpath(os.path.join(_HERE, "..", "..")) # 05_deliverables_mvp/crm/ if _CRM not in sys.path: sys.path.insert(0, _CRM) from workflow_vente.wflib.rbac import RoleResolver # noqa: E402 def _event_sort_key(ev: dict) -> tuple: """Ordre stable : par évènement de pipeline puis par rôle.""" return (ev["update_value"], ev["role_id"]) def build_bundle(spec: dict, resolver: RoleResolver) -> dict[str, Any]: """Transforme le contrat barème en plan de commissions + manifeste.""" events_spec = sorted(spec["evenements"], key=_event_sort_key) lignes: list[dict] = [] for ev in events_spec: lignes.append({ "update_value": ev["update_value"], "role_id": ev["role_id"], "erpnext_role_name": resolver.erpnext_name(ev["role_id"]), "base_field": ev["base_field"], "libelle": ev["libelle"], # Taux jamais fabriqué (#6) : recopié verbatim (null tant qu'à confirmer). "taux_pct": ev.get("taux_pct"), "source": ev.get("source"), "a_confirmer": bool(ev.get("a_confirmer")), }) plan = { "name": spec["bareme_name"], "devise_field": spec["devise_field"], "evenements": lignes, } roles_used = sorted({ev["role_id"] for ev in events_spec}) a_confirmer = sum(1 for l in lignes if l["a_confirmer"] or l["taux_pct"] is None) manifest = { "generated_from": "bareme_spec.json", "rbac_source": "rbac_50_roles.json", "workflow_source": "workflow_vente/workflow_vente_spec.json", "doctype_source": "dossier_vente/doctype_spec.json", "source_version": spec["version"], "bareme_name": spec["bareme_name"], "counts": { "evenements": len(lignes), "roles": len(roles_used), "taux_a_confirmer": a_confirmer, }, "roles_rbac_utilises": [ {"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)} for rid in roles_used ], # Rappel anti-invention (#6) : aucun taux n'est fixé en-repo. La Direction # renseigne `taux_pct` + `source` avant tout calcul de commission réel. "note_taux": ( "Aucun taux de commission n'est documenté dans CLAUDE.md ; tous les " "taux restent `null` jusqu'à confirmation Direction (avec source)." ), } return {"manifest": manifest, "commission_plan": plan}