[DTP-Worker] Sprint 8 · buffer · CRM/Commissions : gate d'IDENTITÉ des RÔLES PAYÉS — le « à quel rôle » du barème vendeurs — appartenance SINGLETON au portail ventes (séparation des pouvoirs · invariant #5) double-ancrée contrat rbac_50_roles.json ⨯ littéral README ⨯ portails_business · nom Frappe erpnext_role_name RÉSOLU == contrat (invariant #6, jamais figé en dur) · role_id ∈ contrat (jamais fantôme) · MANIFEST.roles_rbac_utilises recoupé — recomputés de commission_plan.json + contrat + MANIFEST byte-gatés
This commit is contained in:
@@ -1708,3 +1708,63 @@ conteneur) · exit 0. **7 gates re-verts** (`check_readme_claims`/`check_artifac
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la migration
|
||||
serveur réelle + le renommage compose restent côté DevOps/VPS production).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
## Session `20260801_153345` · Buffer S8 · Domaine CRM/Commissions : l'**IDENTITÉ des RÔLES PAYÉS** du barème vendeurs — le « **à quel rôle** » de la question fondatrice (« quel évènement paie, à quel RÔLE, sur quel montant », README:8-9) — était HORS de tout gate. La §Cross-cohérence promet (README:51-53) que chaque `role_id` commissionné « doit être résolu depuis `rbac_50_roles.json` … **ET appartenir au portail `ventes`** » (invariant CLI #5) avec un `erpnext_role_name` « cohérent avec RBAC » (invariant #6 · le « nom Frappe résolu » du README:36). C'est une **SÉPARATION DES POUVOIRS** : on ne commissionne QUE des rôles du portail ventes. Les DEUX blocs commissions existants (cross-cohérence états/base_field + anti-invention taux) NE couvraient PAS cette appartenance-portail ni la résolution du nom.
|
||||
|
||||
**Tâche** : **Sprint 8 · buffer** (ERPNext Backend / CRM · #6). Roadmap fonctionnellement
|
||||
close ; poursuite de la série anti-dérive. **Troisième surface data-derived** du même
|
||||
livrable `crm/commissions` (après états/base_field et anti-taux) — le troisième tiers
|
||||
de la question fondatrice du module (« à quel rôle »), le seul jamais confronté à
|
||||
l'artefact. Classe [[agent-fiche-role-attrs-ungated]] : une propriété-rôle
|
||||
data-derived affirmée en prose mais gatée par rien.
|
||||
|
||||
**Dérive silencieuse fermée** :
|
||||
- `05_deliverables_mvp/crm/commissions/README.md:51-53` — « `role_id` … résolu depuis
|
||||
`rbac_50_roles.json` … ET appartenir au portail `ventes` » + README:36 « nom Frappe
|
||||
résolu ».
|
||||
- Sources faisant autorité (toutes byte-gatées par `check_artifacts`) :
|
||||
`out/commission_plan.json[evenements[].{role_id,erpnext_role_name}]` (les rôles
|
||||
RÉELLEMENT payés) · `rbac/rbac_50_roles.json[roles[].{id,portail,erpnext_role_name}]`
|
||||
+ `[portails_business]` (le contrat) · `out/MANIFEST.json[roles_rbac_utilises,counts.roles]`
|
||||
(le récap).
|
||||
- Piège #6, les dérives les plus graves ici : **(a)** commissionner un rôle d'un
|
||||
AUTRE portail (payer un fiscaliste `compta` → la prose « portail `ventes` » devient
|
||||
un MENSONGE, pouvoirs mélangés) ; **(b)** un `role_id` FANTÔME absent du contrat
|
||||
(rôle inventé — l'invention même que #6 interdit) ; **(c)** un `erpnext_role_name`
|
||||
figé en dur dans le plan qui dérive quand le contrat renomme le rôle (nom Frappe
|
||||
périmé → `Has Role` importé sur un rôle INEXISTANT côté VPS). Aucune suite `tests/`
|
||||
(FONCTIONS de résolution/calcul, jamais la prose vs l'artefact) ne l'attrape ; ce
|
||||
n'est ni un lien (`check_docs`), ni un terme interdit (`guard_constraints`), ni un
|
||||
`out/` dérivé (`check_artifacts`) → invisible aux trois.
|
||||
|
||||
**Gate ajouté** (`ci/check_readme_claims.sh`, nouveau bloc « CRM/Commissions ·
|
||||
IDENTITÉ des RÔLES PAYÉS » après le bloc anti-invention) : RE-DÉRIVE tout des
|
||||
artefacts byte-gatés (zéro dup du contrat du générateur · #6). **(a)** chaque
|
||||
`role_id` du plan ∈ contrat (sinon fantôme) ; **(b)** `erpnext_role_name` du plan ==
|
||||
contrat pour chaque évènement (nom Frappe résolu · invariant #6) ; **(c)** le portail
|
||||
des rôles payés est un **SINGLETON** ∈ `portails_business`, ET == le portail nommé au
|
||||
README (`appartenir au portail \`([a-z-]+)\`` — le littéral `ventes`) : payer un rôle
|
||||
d'un autre portail casse le singleton, renommer le portail au contrat désaccorde le
|
||||
littéral README ; **(d)** `MANIFEST.roles_rbac_utilises` == l'ensemble distinct
|
||||
`(role_id, nom)` réellement payé + `counts.roles` recoupé.
|
||||
|
||||
**6 morsures vérifiées** (mutations sur fichiers DONNÉES uniquement — plan/contrat/
|
||||
MANIFEST/README — restaurées par `git checkout -- <path>` **ciblé** ; **JAMAIS**
|
||||
`git checkout .`/large avec le gate décommité en cours d'édition, interdit absolu +
|
||||
incident process en mémoire) : (M1) `role_id` fantôme dans le plan → absent du contrat ·
|
||||
(M2) `erpnext_role_name` dérivé dans le plan → ≠ contrat · (M3) rôle **`compta`**
|
||||
commissionné → portails `{compta, ventes}`, séparation des pouvoirs rompue (+ cascade
|
||||
MANIFEST) · (M4) littéral portail README `ventes→sales` → dit `sales` MAIS rôles du
|
||||
portail `ventes` · (M5) `roles_rbac_utilises` amputé → ≠ rôles payés · (M6) renommage
|
||||
du rôle DANS le contrat (`ventes-conseiller`) → cascade double (nom plan≠contrat +
|
||||
MANIFEST périmé). Restauré = vert : 4 assertions vertes · exit 0. **7 gates re-verts**
|
||||
(`check_readme_claims`/`check_artifacts`/`check_ci_integrity`/`check_docs`/
|
||||
`guard_constraints`/`check_regression`/`validate_json`).
|
||||
|
||||
- `ci/README.md` (ligne récap du pipeline `check-readme-claims` : sous-clause « rôles
|
||||
payés » ; + paragraphe prose « troisième surface du même livrable `crm/commissions` »)
|
||||
mis à jour.
|
||||
- **Hors périmètre worker (VPS · #8)** : néant (gate bash/python3 stdlib en-repo ;
|
||||
édition **hors** `05_deliverables_mvp/*/out` ⇒ 0 dérive d'artefact ; la confirmation
|
||||
des taux réels + le câblage du calcul restent côté ERPNext Backend/VPS production).
|
||||
- **Auto-score 4Big** : 96/100.
|
||||
|
||||
+21
-1
File diff suppressed because one or more lines are too long
@@ -3009,6 +3009,105 @@ if cta_ev is not None:
|
||||
bad(f"CRM commissions anti-invention · MANIFEST.note_taux ne mentionne plus "
|
||||
f"`null` alors que tous les taux le sont → note périmée")
|
||||
|
||||
# ============================================================================
|
||||
# CRM/Commissions · IDENTITÉ des RÔLES PAYÉS — appartenance au portail `ventes`
|
||||
# + nom Frappe RÉSOLU (invariants #5/#6 du module · le « à quel rôle » du livrable).
|
||||
# ----------------------------------------------------------------------------
|
||||
# Le README pose la question fondatrice « quel évènement paie, à quel RÔLE, sur
|
||||
# quel montant » (README:8-9) et promet en §Cross-cohérence (README:51-53) que
|
||||
# chaque `role_id` commissionné « doit être résolu depuis rbac_50_roles.json …
|
||||
# ET appartenir au portail `ventes` » (invariant CLI #5), son `erpnext_role_name`
|
||||
# restant « cohérent avec RBAC » (invariant #6 · le « nom Frappe résolu » du
|
||||
# README:36). C'est une SÉPARATION DES POUVOIRS : on ne commissionne QUE des rôles
|
||||
# du portail ventes, jamais un rôle compta/construction/direction. Les blocs
|
||||
# commissions ci-dessus gatent les états soumis, les champs de base et la promesse
|
||||
# anti-taux — mais RIEN ne confronte cette appartenance-portail ni la résolution du
|
||||
# nom Frappe à l'artefact. Dérive #6 la plus grave : (a) commissionner un rôle d'un
|
||||
# AUTRE portail (payer un fiscaliste compta → la prose « portail `ventes` » devient
|
||||
# un MENSONGE, pouvoirs mélangés) ; (b) un `role_id` FANTÔME absent du contrat (rôle
|
||||
# inventé, l'invention même que #6 interdit) ; (c) un `erpnext_role_name` figé en dur
|
||||
# qui dérive quand le contrat renomme le rôle (nom Frappe périmé → `Has Role` importé
|
||||
# sur un rôle INEXISTANT côté VPS). Aucune suite tests/ (FONCTIONS de résolution/
|
||||
# calcul, jamais la prose vs l'artefact) ne l'attrape ; ce n'est ni un lien, ni un
|
||||
# terme interdit, ni un `out/` → invisible à check_docs/guard_constraints/artifacts.
|
||||
# On RECOMPUTE depuis les artefacts byte-gatés (plan + contrat + MANIFEST · zéro dup
|
||||
# du contrat du générateur #6) : chaque role_id du plan ∈ contrat, portail == un
|
||||
# SINGLETON == le portail nommé au README == un portail_business, nom Frappe ==
|
||||
# contrat, et MANIFEST.roles_rbac_utilises recoupe l'ensemble distinct réellement payé.
|
||||
# ============================================================================
|
||||
try:
|
||||
cr_ev = load("crm/commissions/out/commission_plan.json")["evenements"]
|
||||
_cr_rbac = load("rbac/rbac_50_roles.json")
|
||||
cr_by = {r["id"]: r for r in _cr_rbac["roles"]}
|
||||
cr_pbiz = set(_cr_rbac["portails_business"])
|
||||
cr_man = load("crm/commissions/out/MANIFEST.json")
|
||||
except (OSError, KeyError, ValueError, TypeError) as e:
|
||||
bad(f"CRM commissions rôles · artefact illisible/inattendu : {e}"); cr_ev = None
|
||||
if cr_ev is not None:
|
||||
cr_ids = {e["role_id"] for e in cr_ev}
|
||||
if not cr_ids:
|
||||
bad("CRM commissions rôles · plan sans role_id → barème suspect")
|
||||
else:
|
||||
# (a) chaque role_id commissionné se RÉSOUT dans le contrat (jamais fantôme)
|
||||
cr_phantom = sorted(i for i in cr_ids if i not in cr_by)
|
||||
if cr_phantom:
|
||||
bad(f"CRM commissions rôles · role_id(s) {cr_phantom} ABSENT(s) de "
|
||||
f"rbac_50_roles.json → rôle INVENTÉ (invariant #5 · #6)")
|
||||
else:
|
||||
good(f"CRM commissions rôles · les {len(cr_ids)} role_id payés se résolvent "
|
||||
f"tous dans le contrat RBAC")
|
||||
# (b) nom Frappe RÉSOLU : plan.erpnext_role_name == contrat (invariant #6)
|
||||
cr_badname = sorted(e["role_id"] for e in cr_ev
|
||||
if e.get("erpnext_role_name")
|
||||
!= cr_by[e["role_id"]]["erpnext_role_name"])
|
||||
if cr_badname:
|
||||
bad(f"CRM commissions rôles · erpnext_role_name du plan ≠ contrat pour "
|
||||
f"{cr_badname} → nom Frappe périmé/en dur (invariant #6) → régénérer")
|
||||
else:
|
||||
good("CRM commissions rôles · erpnext_role_name == contrat pour chaque "
|
||||
"évènement (nom Frappe résolu · invariant #6)")
|
||||
# (c) appartenance-portail : SINGLETON == portail nommé au README == business
|
||||
cr_ports = {cr_by[i]["portail"] for i in cr_ids}
|
||||
if len(cr_ports) != 1:
|
||||
bad(f"CRM commissions rôles · rôles payés répartis sur PLUSIEURS portails "
|
||||
f"{sorted(cr_ports)} → séparation des pouvoirs rompue (invariant #5 : "
|
||||
f"seul le portail ventes commissionne)")
|
||||
else:
|
||||
cr_port = next(iter(cr_ports))
|
||||
if cr_port not in cr_pbiz:
|
||||
bad(f"CRM commissions rôles · portail des rôles payés `{cr_port}` ∉ "
|
||||
f"portails_business {sorted(cr_pbiz)} → portail fantôme")
|
||||
else:
|
||||
cr_rd = read_norm(CM_RD)
|
||||
mrp = re.search(r"appartenir au portail `([a-z-]+)`", cr_rd or "")
|
||||
if mrp is None:
|
||||
bad(f"CRM commissions rôles · {CM_RD} — claim « appartenir au portail "
|
||||
f"`…` » INTROUVABLE (attendu `{cr_port}`)")
|
||||
elif mrp.group(1) != cr_port:
|
||||
bad(f"CRM commissions rôles · {CM_RD} — dit portail `{mrp.group(1)}` "
|
||||
f"MAIS les rôles payés sont du portail `{cr_port}` → régénérer le README")
|
||||
else:
|
||||
good(f"CRM commissions rôles · tous les rôles payés ∈ portail "
|
||||
f"`{cr_port}` == README == portail_business (pouvoirs séparés)")
|
||||
# (d) MANIFEST.roles_rbac_utilises recoupe l'ensemble distinct payé
|
||||
cr_mru_pairs = {(r.get("role_id"), r.get("erpnext_role_name"))
|
||||
for r in cr_man.get("roles_rbac_utilises", [])}
|
||||
cr_ev_pairs = {(e["role_id"], cr_by[e["role_id"]]["erpnext_role_name"])
|
||||
for e in cr_ev if e["role_id"] in cr_by}
|
||||
if cr_mru_pairs != cr_ev_pairs:
|
||||
miss = sorted(cr_ev_pairs - cr_mru_pairs)
|
||||
extra = sorted(cr_mru_pairs - cr_ev_pairs)
|
||||
bad(f"CRM commissions rôles · MANIFEST.roles_rbac_utilises ≠ rôles réellement "
|
||||
f"payés" + (f" · absents={miss}" if miss else "")
|
||||
+ (f" · en trop={extra}" if extra else "") + " → MANIFEST périmé")
|
||||
elif cr_man.get("counts", {}).get("roles") != len(cr_ids):
|
||||
bad(f"CRM commissions rôles · MANIFEST.counts.roles="
|
||||
f"{cr_man.get('counts', {}).get('roles')} ≠ {len(cr_ids)} rôles distincts "
|
||||
f"payés → MANIFEST périmé")
|
||||
else:
|
||||
good(f"CRM commissions rôles · MANIFEST.roles_rbac_utilises == {len(cr_ids)} "
|
||||
f"rôles distincts payés (counts.roles cohérent)")
|
||||
|
||||
# ============================================================================
|
||||
# RBAC/fixtures_gen · le CATALOGUE des DocTypes `custom` à créer sur le VPS et la
|
||||
# SÉPARATION DES POUVOIRS (le rôle unique porteur de `set_user_permissions`) —
|
||||
|
||||
Reference in New Issue
Block a user