Files
oto-enterprise-os-dtp/05_deliverables_mvp/crm/financement_bancaire/finlib/builder.py
T
Claude Code DTP Worker 48276ee916 [DTP-Worker 20260806_031432] FIX générateur — crm/financement_bancaire MANIFEST.roles_rbac_utilises répétait ventes-conseiller (dup ungaté, liste per-slot vs ensemble distinct)
Défaut réel/reproductible (pas stale, byte-repro vert) : finlib/builder.py:89
faisait [roles_resolved[k] for k in sorted(roles_resolved)], itérant les 4 SLOTS
logiques de spec["roles"] — 2 slots (conseiller_wag ET validation_dossier) pointent
le même rôle ventes-conseiller → 4 entrées pour 3 rôles distincts. Preuve bug (pas
per-slot voulu) : la sortie drope la clé de slot (cle), donc le doublon ne porte
aucune info distinctive ; sémantique documentée README.md:54 « rôles RBAC utilisés »
+ ci/README.md:1100 « ensemble distinct » ; les 3 modules frères produisent tous un
ensemble distinct (confotur 3/3, commissions 4/4, workflow_vente 7/7) — financement
seul outlier 4/3.

Fix (aligné idiome commissions) : ensemble distinct trié par role_id via
sorted({rr["role_id"] for rr in roles_resolved.values()}). Artefact régénéré → 3/3
distinct. 0 gate ajouté (#5 — champ lu par AUCUN gate financement ni consommateur
externe ; convention distinctness déjà tenue par les frères). 0 chiffre inventé (#6).
run_ci 33 PASS · 0 FAIL · 0 SKIP (byte-repro vert avec nouvel artefact, 35 tests verts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 03:22:44 +00:00

110 lines
4.5 KiB
Python

"""Assemblage du bundle d'artefacts Financement Bancaire depuis le contrat.
Entrée : `financement_spec.json` + 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 (`ci/check_artifacts.sh`).
Le builder n'invente rien (#6) : banques, documents, autorisations, sections,
conditions du gate proviennent du contrat ; les noms de rôle proviennent du
contrat RBAC ; l'exemple de gate-status est CALCULÉ par `finlib.gate` depuis le
dossier d'exemple du contrat (donc auditable, pas fabriqué).
"""
from __future__ import annotations
from typing import Any
from . import gate
from .rbac import RoleResolver
def build_bundle(spec: dict, resolver: RoleResolver) -> dict[str, Any]:
"""Transforme le contrat en bundle d'artefacts prêts à consommer."""
cfg = gate.GateConfig.from_spec(spec)
# --- rôles résolus (id → nom Frappe) --------------------------------------
roles_resolved = {
cle: {"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
for cle, rid in sorted(spec["roles"].items())
}
# --- banques (ordre du contrat = ordre d'affichage) -----------------------
banques = list(spec["banques"])
# --- sections triées par `ordre` -----------------------------------------
sections = sorted(spec["sections"], key=lambda s: s["ordre"])
documents = list(spec["documents"])
autorisations = list(spec["autorisations"])
# --- spec du gate (conditions + bannière + endpoints + rôle validation) ---
gate_spec = {
"endpoint": spec["gate"]["endpoint"],
"submit_endpoint": spec["gate"]["submit_endpoint"],
"condition_keys": list(gate.CONDITION_KEYS),
"conditions": sorted(spec["gate"]["conditions"], key=lambda c: c["ordre"]),
"banniere": spec["gate"]["banniere"],
"role_validation": roles_resolved["validation_dossier"],
"workflow_suivi_banque": list(spec["workflow_suivi_banque"]),
}
# --- exemple de gate-status CALCULÉ (auditable, pas inventé) ---------------
exemple = spec["dossier_exemple"]
can_submit, reasons = gate.can_submit_dossier(exemple, cfg)
gate_status_example = {
"dossier_id": exemple["id"],
"projet": exemple["projet"],
"unite": exemple["unite"],
"banque_choisie": exemple["banque_choisie"],
"gate_status": gate.gate_status(exemple, cfg),
"can_submit": can_submit,
"reasons": reasons,
}
# --- comptes des documents requis par type de résidence -------------------
docs_requis = {
rt["code"]: len(gate.required_document_ids(rt["code"], cfg))
for rt in spec["apport"]["residence_types"]
}
manifest = {
"generated_from": "financement_spec.json",
"source_directive": spec["source_directive"],
"rbac_source": "rbac_50_roles.json",
"source_version": spec["version"],
"module": spec["module"],
"titre": spec["titre"],
"counts": {
"banques": len(banques),
"sections": len(sections),
"documents": len(documents),
"documents_requis_resident": docs_requis.get("resident_rd", 0),
"documents_requis_etranger": docs_requis.get("etranger", 0),
"autorisations": len(autorisations),
"gate_conditions": len(gate_spec["conditions"]),
"banniere_lignes": len(gate_spec["banniere"]["lignes"]),
"workflow_suivi_etapes": len(gate_spec["workflow_suivi_banque"]),
},
# Ensemble DISTINCT des rôles RBAC réellement utilisés. Plusieurs slots
# logiques peuvent pointer le même rôle (ex. conseiller_wag ET
# validation_dossier == ventes-conseiller) : on dédoublonne par role_id
# et on trie par role_id, comme les modules frères (commissions,
# confotur, workflow_vente). Sans ce dédoublonnage, la liste répétait le
# rôle et contredisait sa sémantique « rôles utilisés » (ensemble).
"roles_rbac_utilises": [
{"role_id": rid, "erpnext_role_name": resolver.erpnext_name(rid)}
for rid in sorted({rr["role_id"] for rr in roles_resolved.values()})
],
}
return {
"manifest": manifest,
"banques": banques,
"sections": sections,
"documents": documents,
"autorisations": autorisations,
"gate_spec": gate_spec,
"gate_status_example": gate_status_example,
}