"""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, }