[DTP-Worker 20260803_090713] Auto exec · session 20260803_090713
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
CI / Contraintes NON-NÉGOCIABLES (CLAUDE.md) (push) Has been cancelled
CI / Validation JSON (schémas Faisabilité) (push) Has been cancelled
CI / Qualité documentaire (liens + 4Big) (push) Has been cancelled
CI / Reproductibilité des artefacts out/ (build == commité) (push) Has been cancelled
CI / Fraîcheur matrice de régression (run == commité) (push) Has been cancelled
CI / Intégrité du câblage CI (gate agrège tout · gates statiques verrouillés) (push) Has been cancelled
CI / Intégrité des chiffres du README (valeur == artefact cité · (push) Has been cancelled
CI / Publiciste · parser + schéma + generator (unittest) (push) Has been cancelled
CI / RBAC · 50 rôles + schéma (unittest) (push) Has been cancelled
CI / Faisabilité · générateur 4 volets + round-trip (unittest) (push) Has been cancelled
CI / RBAC · fixtures ERPNext (Role + Custom DocPerm) (push) Has been cancelled
CI / RBAC · plan User Permission (row-level) (push) Has been cancelled
CI / RBAC · Role Profile (bundles par portail) (push) Has been cancelled
CI / RBAC · run-book d'application unifié (agrégat 3 volets) (push) Has been cancelled
CI / Faisabilité · dossier bancable trilingue FR/EN/ES (push) Has been cancelled
CI / CRM · workflow vente ERPNext (lead → CONFOTUR) (push) Has been cancelled
CI / CRM · DocType porteur OTO Dossier Vente (push) Has been cancelled
CI / CRM · barème commissions vendeurs (push) Has been cancelled
CI / CRM · Financement Bancaire (gate hypothécaire RD) (push) Has been cancelled
CI / Fiscal · e-CF DGII (Compupar) (push) Has been cancelled
CI / Frontend · Workspaces 5 portails rôle (push) Has been cancelled
CI / Legal · DocType CONFOTUR Application (push) Has been cancelled
CI / QA · Audit 5D conformité (push) Has been cancelled
CI / SEO · mots-clés trilingues + schema.org + hreflang (push) Has been cancelled
CI / Chat OTOIA · montage par portail (Custom Block) (push) Has been cancelled
CI / QA · Audit 4Big (95+/100 sur 100% deliverables) (push) Has been cancelled
CI / Démo · Scénarios (run-sheet P07 banquier / P05 client) (push) Has been cancelled
CI / QA · Matrice de régression exhaustive (Sprint 8) (push) Has been cancelled
CI / DevOps · Run-book de déploiement VPS unifié (Sprint 8) (push) Has been cancelled
CI / QA · Matrice d'acceptation / traçabilité MVP (Sprint 8) (push) Has been cancelled
CI / Mobile · config app Expo/EAS (navigation par rôle) (push) Has been cancelled
CI / E2E baseline Playwright (manuel) (push) Has been cancelled
CI / Gate qualité (agrégat) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
"""Tests du module Financement Bancaire — cœur métier (gate) + générateur.
|
||||
|
||||
Deux familles :
|
||||
1. Cœur métier `finlib.gate` — calcul d'apport 20/30 + gate check 4 conditions.
|
||||
Fonctions pures → scénarios exhaustifs (chaque condition qui bloque, cas
|
||||
nominal qui passe, frontière apport).
|
||||
2. Générateur — validate/build, déterminisme (build == build), schéma +
|
||||
12 invariants, résolution RBAC réelle.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_MODULE = os.path.normpath(os.path.join(_HERE, ".."))
|
||||
_DELIVERABLES = os.path.normpath(os.path.join(_MODULE, "..", ".."))
|
||||
|
||||
sys.path.insert(0, _MODULE)
|
||||
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))
|
||||
|
||||
from finlib import gate # noqa: E402
|
||||
from finlib.builder import build_bundle # noqa: E402
|
||||
from finlib.rbac import RoleResolver # noqa: E402
|
||||
|
||||
_SPEC_PATH = os.path.join(_MODULE, "financement_spec.json")
|
||||
_GEN = os.path.join(_MODULE, "financement_bancaire_gen.py")
|
||||
|
||||
|
||||
def _load_spec():
|
||||
with open(_SPEC_PATH, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
class ApportTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cfg = gate.GateConfig.from_spec(_load_spec())
|
||||
|
||||
def test_taux_resident_20_etranger_30(self):
|
||||
self.assertEqual(self.cfg.taux_apport_pct["resident_rd"], 20.0)
|
||||
self.assertEqual(self.cfg.taux_apport_pct["etranger"], 30.0)
|
||||
|
||||
def test_apport_requis_resident(self):
|
||||
self.assertAlmostEqual(gate.apport_requis_usd(100000, "resident_rd", self.cfg), 20000.0)
|
||||
|
||||
def test_apport_requis_etranger(self):
|
||||
self.assertAlmostEqual(gate.apport_requis_usd(100000, "etranger", self.cfg), 30000.0)
|
||||
|
||||
def test_apport_requis_type_inconnu_leve(self):
|
||||
with self.assertRaises(KeyError):
|
||||
gate.apport_requis_usd(100000, "martien", self.cfg)
|
||||
|
||||
def test_apport_verse_somme_paiements(self):
|
||||
dossier = {"paiements": [{"montant_verse_usd": 5000}, {"montant_verse_usd": 2500.5}]}
|
||||
self.assertAlmostEqual(gate.apport_verse_usd(dossier), 7500.5)
|
||||
|
||||
def test_apport_verse_aucun_paiement(self):
|
||||
self.assertEqual(gate.apport_verse_usd({}), 0.0)
|
||||
|
||||
|
||||
class RequiredDocumentsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.cfg = gate.GateConfig.from_spec(_load_spec())
|
||||
|
||||
def test_etranger_exige_plus_que_resident(self):
|
||||
res = set(gate.required_document_ids("resident_rd", self.cfg))
|
||||
etr = set(gate.required_document_ids("etranger", self.cfg))
|
||||
self.assertTrue(res < etr) # sous-ensemble strict
|
||||
|
||||
def test_resident_ninclut_pas_docs_etrangers(self):
|
||||
res = gate.required_document_ids("resident_rd", self.cfg)
|
||||
self.assertNotIn("lettre_origine_fonds", res)
|
||||
self.assertNotIn("passeport_2cotes", res)
|
||||
|
||||
def test_etranger_inclut_docs_etrangers(self):
|
||||
etr = gate.required_document_ids("etranger", self.cfg)
|
||||
self.assertIn("lettre_origine_fonds", etr)
|
||||
self.assertIn("refs_bancaires_intl", etr)
|
||||
|
||||
def test_document_optionnel_exclu(self):
|
||||
# titre_definitif is_required=false → jamais dans la liste des exigés.
|
||||
self.assertNotIn("titre_definitif", gate.required_document_ids("etranger", self.cfg))
|
||||
|
||||
|
||||
def _dossier_complet(cfg, spec):
|
||||
"""Un dossier qui satisfait les 4 conditions (résident RD, 100 000 USD)."""
|
||||
required = gate.required_document_ids("resident_rd", cfg)
|
||||
return {
|
||||
"id": "T-OK",
|
||||
"prix_usd": 100000,
|
||||
"residence_type": "resident_rd",
|
||||
"banque_choisie": "banreservas",
|
||||
"paiements": [{"montant_verse_usd": 20000}], # 20 % pile
|
||||
"documents": [{"id": d, "statut": "Validé WAG"} for d in required],
|
||||
"autorisations": [{"id": a["id"], "signature_date": "2026-08-01"} for a in spec["autorisations"]],
|
||||
"wag_validated_by": "ventes-conseiller:amelie",
|
||||
}
|
||||
|
||||
|
||||
class GateCheckTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.spec = _load_spec()
|
||||
self.cfg = gate.GateConfig.from_spec(self.spec)
|
||||
|
||||
def test_dossier_complet_soumissible(self):
|
||||
ok, reasons = gate.can_submit_dossier(_dossier_complet(self.cfg, self.spec), self.cfg)
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual(reasons, [])
|
||||
|
||||
def test_apport_pile_au_seuil_passe(self):
|
||||
# Exactement 20 % → apport complet (frontière incluse).
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertTrue(status["conditions"]["apport_initial_complet"]["ok"])
|
||||
self.assertEqual(status["conditions"]["apport_initial_complet"]["percent"], 100.0)
|
||||
|
||||
def test_apport_manquant_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["paiements"] = [{"montant_verse_usd": 19999}]
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Apport initial incomplet" in r for r in reasons))
|
||||
c = gate.gate_status(d, self.cfg)["conditions"]["apport_initial_complet"]
|
||||
self.assertAlmostEqual(c["remaining"], 1.0)
|
||||
|
||||
def test_document_non_valide_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"][0]["statut"] = "Déposé" # déposé mais pas validé WAG
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Documents exigés incomplets" in r for r in reasons))
|
||||
|
||||
def test_document_envoye_banque_est_recevable(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"][0]["statut"] = "Envoyé banque"
|
||||
ok, _ = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertTrue(ok)
|
||||
|
||||
def test_document_manquant_compte_comme_non_depose(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["documents"] = d["documents"][:-1] # retire un requis
|
||||
c = gate.gate_status(d, self.cfg)["conditions"]["documents_exiges"]
|
||||
self.assertFalse(c["ok"])
|
||||
self.assertEqual(c["validated"], c["total"] - 1)
|
||||
|
||||
def test_autorisation_non_signee_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["autorisations"][0]["signature_date"] = None
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Autorisations non signées" in r for r in reasons))
|
||||
|
||||
def test_validation_wag_manquante_bloque(self):
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
d["wag_validated_by"] = None
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertTrue(any("Validation WAG manquante" in r for r in reasons))
|
||||
|
||||
def test_quatre_conditions_toutes_ko(self):
|
||||
# Dossier vide → 4 raisons, 0 condition complétée.
|
||||
d = {"id": "T-VIDE", "prix_usd": 100000, "residence_type": "etranger"}
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertFalse(status["can_submit"])
|
||||
self.assertEqual(status["conditions_completed"], 0)
|
||||
ok, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertFalse(ok)
|
||||
self.assertEqual(len(reasons), 4)
|
||||
|
||||
def test_gate_status_forme_endpoint(self):
|
||||
# Forme conforme au contrat de l'endpoint gate-status.
|
||||
d = _dossier_complet(self.cfg, self.spec)
|
||||
status = gate.gate_status(d, self.cfg)
|
||||
self.assertIn("can_submit", status)
|
||||
self.assertEqual(set(status["conditions"]), set(gate.CONDITION_KEYS))
|
||||
for k in gate.CONDITION_KEYS:
|
||||
self.assertIn("percent", status["conditions"][k])
|
||||
|
||||
def test_reasons_ordre_canonique(self):
|
||||
d = {"id": "T", "prix_usd": 100000, "residence_type": "etranger"}
|
||||
_, reasons = gate.can_submit_dossier(d, self.cfg)
|
||||
self.assertIn("Apport", reasons[0])
|
||||
self.assertIn("Documents", reasons[1])
|
||||
self.assertIn("Autorisations", reasons[2])
|
||||
self.assertIn("Validation WAG", reasons[3])
|
||||
|
||||
|
||||
class BuilderTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.spec = _load_spec()
|
||||
self.resolver = RoleResolver.from_path()
|
||||
self.bundle = build_bundle(self.spec, self.resolver)
|
||||
|
||||
def test_bundle_cles(self):
|
||||
for k in ("manifest", "banques", "sections", "documents", "autorisations", "gate_spec", "gate_status_example"):
|
||||
self.assertIn(k, self.bundle)
|
||||
|
||||
def test_role_validation_resolu(self):
|
||||
rv = self.bundle["gate_spec"]["role_validation"]
|
||||
self.assertEqual(rv["role_id"], "ventes-conseiller")
|
||||
self.assertEqual(rv["erpnext_role_name"], "OTO Ventes Conseiller")
|
||||
|
||||
def test_role_inconnu_leve(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["roles"]["validation_dossier"] = "role-inexistant"
|
||||
with self.assertRaises(KeyError):
|
||||
build_bundle(bad, self.resolver)
|
||||
|
||||
def test_sections_triees_par_ordre(self):
|
||||
ordres = [s["ordre"] for s in self.bundle["sections"]]
|
||||
self.assertEqual(ordres, sorted(ordres))
|
||||
self.assertEqual(self.bundle["sections"][0]["id"], "apport_initial")
|
||||
|
||||
def test_exemple_est_bloque(self):
|
||||
# Le dossier d'exemple DOIT démontrer un blocage (valeur pédagogique).
|
||||
self.assertFalse(self.bundle["gate_status_example"]["can_submit"])
|
||||
self.assertTrue(self.bundle["gate_status_example"]["reasons"])
|
||||
|
||||
def test_deterministe(self):
|
||||
again = build_bundle(_load_spec(), RoleResolver.from_path())
|
||||
self.assertEqual(
|
||||
json.dumps(self.bundle, sort_keys=True, ensure_ascii=False),
|
||||
json.dumps(again, sort_keys=True, ensure_ascii=False),
|
||||
)
|
||||
|
||||
|
||||
class GeneratorCliTest(unittest.TestCase):
|
||||
def _run(self, *argv):
|
||||
return subprocess.run(
|
||||
[sys.executable, _GEN, *argv],
|
||||
capture_output=True, text=True, cwd=_MODULE,
|
||||
)
|
||||
|
||||
def test_validate_ok(self):
|
||||
r = self._run("validate")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("12 invariants", r.stdout)
|
||||
|
||||
@staticmethod
|
||||
def _read(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
def test_build_reproductible(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
r1 = self._run("build", "-o", d)
|
||||
self.assertEqual(r1.returncode, 0, r1.stderr)
|
||||
first = {f: self._read(os.path.join(d, f)) for f in os.listdir(d)}
|
||||
r2 = self._run("build", "-o", d)
|
||||
self.assertEqual(r2.returncode, 0, r2.stderr)
|
||||
second = {f: self._read(os.path.join(d, f)) for f in os.listdir(d)}
|
||||
self.assertEqual(first, second)
|
||||
|
||||
def test_build_egale_commite(self):
|
||||
# L'artefact commité out/ doit être identique à un build frais (miroir
|
||||
# local du gate ci/check_artifacts.sh).
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.assertEqual(self._run("build", "-o", d).returncode, 0)
|
||||
for fname in ("banques.json", "sections.json", "documents.json",
|
||||
"autorisations.json", "gate_spec.json",
|
||||
"gate_status_example.json", "MANIFEST.json"):
|
||||
fresh = self._read(os.path.join(d, fname))
|
||||
committed_path = os.path.join(_MODULE, "out", fname)
|
||||
if os.path.exists(committed_path):
|
||||
self.assertEqual(fresh, self._read(committed_path),
|
||||
f"{fname} a dérivé de sa source")
|
||||
|
||||
|
||||
class InvariantGuardTest(unittest.TestCase):
|
||||
"""Chaque invariant du générateur DOIT mordre une mutation (anti-régression)."""
|
||||
|
||||
def setUp(self):
|
||||
sys.path.insert(0, _MODULE)
|
||||
import financement_bancaire_gen as gen # noqa
|
||||
self.gen = gen
|
||||
self.spec = _load_spec()
|
||||
self.resolver = RoleResolver.from_path()
|
||||
|
||||
def _errors(self, spec):
|
||||
bundle = build_bundle(spec, self.resolver)
|
||||
return self.gen._validate_bundle(spec, bundle)
|
||||
|
||||
def test_baseline_clean(self):
|
||||
self.assertEqual(self._errors(self.spec), [])
|
||||
|
||||
def test_taux_ordre_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["apport"]["residence_types"][0]["taux_apport_pct"] = 40 # résident > étranger
|
||||
self.assertTrue(any("Taux d'apport" in e for e in self._errors(bad)))
|
||||
|
||||
def test_section_gating_deplacee_mordue(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["sections"][0]["gating"] = False # plus aucune section gating
|
||||
self.assertTrue(any("gating" in e for e in self._errors(bad)))
|
||||
|
||||
def test_id_duplique_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["banques"].append(dict(bad["banques"][0]))
|
||||
self.assertTrue(any("dupliqué" in e for e in self._errors(bad)))
|
||||
|
||||
def test_ltv_incoherent_mordu(self):
|
||||
bad = copy.deepcopy(self.spec)
|
||||
bad["banques"][1]["ltv_min_pct"] = 99 # min > max (85)
|
||||
self.assertTrue(any("LTV" in e for e in self._errors(bad)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user