[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

This commit is contained in:
Claude Code DTP Worker
2026-08-03 09:22:13 +00:00
parent 1e8cce22a0
commit 9b502e67fe
35 changed files with 2066 additions and 33 deletions
+22 -1
View File
@@ -311,6 +311,27 @@ jobs:
- name: Tests générateur barème commissions - name: Tests générateur barème commissions
run: python3 -m unittest discover -s tests -v run: python3 -m unittest discover -s tests -v
# --------------------------------------------------------------------------
# Financement Bancaire (Sprint 5 · CRM natif · roadmap L52 « workflow vente
# end-to-end » · volet financement · DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_
# 20260803.md). Parcours hypothécaire immobilier RD : contrat banques/documents/
# autorisations/sections + CŒUR MÉTIER du gate check 4 conditions (apport initial
# 20/30 · documents validés WAG · autorisations signées · validation WAG). Anti-
# invention (#6) : taux d'apport et rôles depuis le contrat + RBAC (jamais codés) ;
# taux/LTV bancaires non énoncés = null a_confirmer. Exemple de gate CALCULÉ,
# auditable. Schéma de sortie + 12 invariants. stdlib pur, zéro pip.
# --------------------------------------------------------------------------
crm-financement-bancaire-tests:
name: CRM · Financement Bancaire (gate hypothécaire RD)
runs-on: ubuntu-latest
defaults:
run:
working-directory: 05_deliverables_mvp/crm/financement_bancaire
steps:
- uses: actions/checkout@v4
- name: Tests générateur Financement Bancaire
run: python3 -m unittest discover -s tests -v
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Intégration e-CF DGII (Sprint 4 · ERPNext Backend « e-CF DGII intégration # Intégration e-CF DGII (Sprint 4 · ERPNext Backend « e-CF DGII intégration
# (Compupar) »). Génère un plan de configuration de facturation électronique # (Compupar) »). Génère un plan de configuration de facturation électronique
@@ -524,7 +545,7 @@ jobs:
gate: gate:
name: Gate qualité (agrégat) name: Gate qualité (agrégat)
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests] needs: [constraints-guard, validate-json, check-docs, check-artifacts, check-regression, check-ci-integrity, check-readme-claims, publiciste-tests, rbac-tests, faisabilite-gen-tests, rbac-fixtures-tests, rbac-userperm-tests, rbac-roleprofile-tests, rbac-applyplan-tests, bancable-tests, crm-workflow-vente-tests, crm-dossier-vente-tests, crm-commissions-tests, crm-financement-bancaire-tests, fiscal-ecf-tests, frontend-portails-tests, legal-confotur-tests, qa-audit-5d-tests, seo-tests, chat-otoia-tests, qa-audit-4big-tests, demo-scenario-tests, qa-regression-tests, devops-deploy-runbook-tests, qa-acceptance-tests, mobile-app-config-tests]
steps: steps:
- name: Résultat - name: Résultat
run: echo "✅ Gate qualité 4Big franchi — tous les checks verts." run: echo "✅ Gate qualité 4Big franchi — tous les checks verts."
+1 -1
View File
@@ -21,7 +21,7 @@ et vérifications HTTP restent côté **agent DevOps/ERPNext Backend**.
| Module | Sprint | Rôle | Entrée | Tests | | Module | Sprint | Rôle | Entrée | Tests |
|---|---|---|---|---| |---|---|---|---|---|
| [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) + [`ci/`](../../ci/README.md) | 1 (roadmap L29) | **CI/CD Gitea Actions** : 3 guards blocants (contraintes / JSON / docs) + un job de tests par livrable gated + `gate` agrégateur + `e2e-baseline` | déclenché sur `push`/`pull_request``main` + `workflow_dispatch` | 22 suites gated | | [`.gitea/workflows/ci.yml`](../../.gitea/workflows/ci.yml) + [`ci/`](../../ci/README.md) | 1 (roadmap L29) | **CI/CD Gitea Actions** : 3 guards blocants (contraintes / JSON / docs) + un job de tests par livrable gated + `gate` agrégateur + `e2e-baseline` | déclenché sur `push`/`pull_request``main` + `workflow_dispatch` | 23 suites gated |
| [`05_deliverables_mvp/devops/deploy_runbook/`](../../05_deliverables_mvp/devops/deploy_runbook/README.md) | 8 (roadmap L73) | **Run-book VPS unifié** : graphe de **7 phases ordonnées** couvrant TOUS les modules gated, dépendances inter-phases + confirmations préalables sourcées | `deploy_runbook_gen.py build\|validate` | 29 (dont 14 injections négatives) | | [`05_deliverables_mvp/devops/deploy_runbook/`](../../05_deliverables_mvp/devops/deploy_runbook/README.md) | 8 (roadmap L73) | **Run-book VPS unifié** : graphe de **7 phases ordonnées** couvrant TOUS les modules gated, dépendances inter-phases + confirmations préalables sourcées | `deploy_runbook_gen.py build\|validate` | 29 (dont 14 injections négatives) |
| [`run_ci.sh`](../../run_ci.sh) | 8 (roadmap L73 · « regression exhaustifs ») | **Miroir LOCAL du gate** : rejoue en une commande, hors runner et hors réseau, l'ensemble exact des checks de `gate.needs` | `bash run_ci.sh` (outil, non un gate) | — (voir ci-dessous) | | [`run_ci.sh`](../../run_ci.sh) | 8 (roadmap L73 · « regression exhaustifs ») | **Miroir LOCAL du gate** : rejoue en une commande, hors runner et hors réseau, l'ensemble exact des checks de `gate.needs` | `bash run_ci.sh` (outil, non un gate) | — (voir ci-dessous) |
+1 -1
View File
@@ -37,7 +37,7 @@ le VPS reçoit — jamais l'inverse.
vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent** vente, le barème commissions, le DocType Dossier Vente et le plan e-CF **résolvent**
leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60 leurs rôles depuis ce fichier, jamais un nom Frappe en dur. Total backend **RBAC 60
tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice tests** (10 + 11 + 12 + 11 + 16) + **e-CF 39 tests**, tous gated dans le CI (matrice
de régression du repo : **569 tests · 22 suites** · verdict `PASS`, source de régression du repo : **604 tests · 23 suites** · verdict `PASS`, source
`qa/regression/out/regression_run.json` — jamais compté à la main · #6). `qa/regression/out/regression_run.json` — jamais compté à la main · #6).
## Hand-off reçus (à appliquer sur le VPS, dans l'ordre) ## Hand-off reçus (à appliquer sur le VPS, dans l'ordre)
+1 -1
View File
@@ -32,7 +32,7 @@ périmètre — un auditeur ne s'audite jamais.
**conformité** normative des hand-off. **conformité** normative des hand-off.
## Verdict agrégé courant (source `qa/regression/out/regression_run.json`) ## Verdict agrégé courant (source `qa/regression/out/regression_run.json`)
**22 suites gated · 569 tests · 569 passés · 0 échec · 0 erreur → verdict `PASS`.** **23 suites gated · 604 tests · 604 passés · 0 échec · 0 erreur → verdict `PASS`.**
Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`, Ce compte est **découvert automatiquement depuis le CI** (`q4lib.registry.parse_ci`,
réutilisé sans duplication) — jamais compté à la main. réutilisé sans duplication) — jamais compté à la main.
@@ -0,0 +1,4 @@
# Caches Python
__pycache__/
*.pyc
# Le dossier out/ EST commité (hand-off frontend/backend direct) — voir README.
@@ -0,0 +1,111 @@
# Financement Bancaire · parcours hypothécaire immobilier RD
**Sprint 5 · CRM natif ERPNext · roadmap L52 (workflow vente end-to-end · volet
financement).** Matérialise la
[`DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md`](../../../DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md)
(Michel, 2026-08-03) : un module **complet** de financement bancaire pour un vrai
parcours hypothécaire dominicain, à la place du module « trop simpliste »
antérieur. Livrable **Phase 1 (P0 · MVP)** : le contrat de données + le **cœur
métier du gate check** + la structure des 6 sections + la bannière critique.
> Ce worker **n'écrit jamais sur le VPS** (contrainte #8). Il produit le contrat
> et les artefacts en-repo ; le backend `/api/hypotheque/*` et le frontend
> `renderHypotheque` réels sont appliqués côté serveur (agents ERPNext Backend /
> Frontend Console). La **Phase 2** (vrais formulaires PDF des 6 banques) attend
> les démarches relationship-manager de Michel — hors périmètre worker.
## Le cœur du livrable · le gate check 4 conditions
L'exigence non-négociable de Michel : **aucun document n'est transmis à la banque**
tant que **les 4 conditions** ne sont pas remplies. La logique vit dans
[`finlib/gate.py`](finlib/gate.py) — fonctions **pures**, testées, sans I/O :
| # | Condition (`key`) | Règle vérifiée |
|---|---|---|
| 1 | `apport_initial_complet` | `Σ paiements ≥ prix × taux`**20 % résident RD · 30 % étranger** (amendement Michel · aussi exigence des banques RD · Ley 189-11). |
| 2 | `documents_exiges` | Tous les documents `is_required` de la banque choisie au statut **« Validé WAG »** ou **« Envoyé banque »**. |
| 3 | `autorisations_signees` | Toutes les autorisations signées (`signature_date` non nulle). |
| 4 | `validation_wag` | Un conseiller WAG référent a validé (`wag_validated_by`). |
```python
from finlib import gate
cfg = gate.GateConfig.from_spec(spec)
ok, reasons = gate.can_submit_dossier(dossier, cfg) # (False, ["Apport initial incomplet : …", …])
status = gate.gate_status(dossier, cfg) # forme de l'endpoint gate-status (barres 0-100 %)
```
Si **une** condition manque, `can_submit` est `False` et `reasons` liste chaque
blocage (ordre canonique). Le backend `POST /…/submit` doit renvoyer **403 + ces
raisons** ; le frontend grise le bouton « Envoyer à la banque » et affiche une
barre de progression par condition. Tant que l'**apport initial** est incomplet,
toutes les sections aval sont **grisées** (section `apport_initial` = seule
section `gating`, en position 1).
## Ce qui est généré (`out/`, commité — hand-off direct)
| Fichier | Contenu |
|---|---|
| `banques.json` | Profils des banques partenaires (type, taux/LTV/durée, spécialités, devises). |
| `sections.json` | Structure accordéon (apport → info achat → choix banque → formulaires → exigences → autorisations → envoi). |
| `documents.json` | Catalogue des documents exigés par catégorie (client / immobilier / étrangers). |
| `autorisations.json` | Autorisations à signer (OTO Sign) + base légale. |
| `gate_spec.json` | Les 4 conditions + **bannière critique** + endpoints + rôle validation résolu + workflow suivi banque. |
| `gate_status_example.json` | Exemple **calculé** (dossier P07 volontairement bloqué) — auditable, jamais fabriqué. |
| `MANIFEST.json` | Traçabilité (sources, comptes dérivés, rôles RBAC utilisés). |
Comptes courants (source de vérité = `MANIFEST.json`) : **6 banques**,
**7 sections**, **15 documents** (requis **9** résident · **13** étranger),
**4 autorisations**, **4 conditions** de gate.
## Anti-invention (#6)
- **Aucun taux d'apport codé en dur** : 20/30 viennent du contrat
(`apport.residence_types`), sourcés de la DIRECTIVE. Changer le contrat change
le gate, sans toucher au code.
- **Aucun taux/LTV bancaire fabriqué** : les fourchettes non données par la
DIRECTIVE restent `null` (`a_confirmer` · démarche relationship manager). Seules
les valeurs que la DIRECTIVE énonce (ex. Scotiabank LTV 70 %, BHD León 30 ans)
sont encodées.
- **Aucun nom de rôle Frappe en dur** : les rôles (`ventes-conseiller` pour la
validation WAG, etc.) sont résolus depuis
[`rbac_50_roles.json`](../../rbac/rbac_50_roles.json) via `finlib/rbac.py`
(source unique · zéro duplication · #5).
## Validation · schéma + 12 invariants
`validate` re-génère le bundle en mémoire et vérifie le schéma de sortie
([`financement.schema.json`](financement.schema.json)) **plus 12 invariants**
métier (unicité des ids ; `0 < résident < étranger ≤ 100` ; fourchettes banque
`min ≤ max` ; rôles résolus dans RBAC ; sections contiguës + section gating en
tête ; gate = 4 conditions alignées sur la bannière ; documents étrangers
strictement additionnels ; statuts recevables ⊆ statuts ; exemple de gate
recalculé cohérent ; comptes du manifeste cohérents). Un invariant qui casse
**refuse** le build (anti-régression).
## Utilisation
```bash
cd 05_deliverables_mvp/crm/financement_bancaire
python3 financement_bancaire_gen.py validate # schéma + 12 invariants
python3 financement_bancaire_gen.py build # écrit les 7 artefacts out/
python3 -m unittest discover -s tests -v # suite (gate + générateur)
```
Sortie **déterministe** (tri stable, aucun horodatage) → l'artefact commité est
reproductible bit-à-bit et vérifié par `ci/check_artifacts.sh`.
## Endpoints backend visés (Phase 1 · à implémenter côté serveur)
Contrat exposé par `gate_spec.json` :
- `GET /api/hypotheque/banques` · profils banques (⇐ `banques.json`)
- `GET /api/hypotheque/dossier/{id}/gate-status` · état des 4 conditions (forme de `gate_status_example.json`)
- `POST /api/hypotheque/dossier/{id}/submit` · **403 + raisons** tant que le gate n'est pas vert
- `GET /api/hypotheque/dossier/{id}/journal` · historique horodaté
## Périmètre
-**Phase 1 (ce module)** : contrat, gate check, sections, bannière, exemple calculé.
-**Phase 2 (hors worker)** : vrais formulaires PDF officiels des 6 banques
(démarche relationship-manager Michel) + implémentation runtime des endpoints /
du `renderHypotheque` côté VPS.
@@ -0,0 +1,165 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Bundle Financement Bancaire (out/)",
"type": "object",
"additionalProperties": true,
"required": ["manifest", "banques", "sections", "documents", "autorisations", "gate_spec", "gate_status_example"],
"properties": {
"manifest": {
"type": "object",
"required": ["generated_from", "source_directive", "rbac_source", "module", "counts", "roles_rbac_utilises"],
"properties": {
"generated_from": {"const": "financement_spec.json"},
"module": {"const": "financement_bancaire"},
"counts": {
"type": "object",
"required": ["banques", "sections", "documents", "autorisations", "gate_conditions", "banniere_lignes", "workflow_suivi_etapes"],
"properties": {
"banques": {"type": "integer", "minimum": 1},
"sections": {"type": "integer", "minimum": 1},
"documents": {"type": "integer", "minimum": 1},
"autorisations": {"type": "integer", "minimum": 1},
"gate_conditions": {"type": "integer", "minimum": 4, "maximum": 4},
"banniere_lignes": {"type": "integer", "minimum": 4, "maximum": 4},
"workflow_suivi_etapes": {"type": "integer", "minimum": 1}
}
},
"roles_rbac_utilises": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["role_id", "erpnext_role_name"],
"properties": {
"role_id": {"type": "string", "minLength": 1},
"erpnext_role_name": {"type": "string", "minLength": 1}
}
}
}
}
},
"banques": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "nom", "type", "specialites", "devises"],
"properties": {
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
"nom": {"type": "string", "minLength": 1},
"type": {"type": "string", "minLength": 1},
"taux_min_pct": {"type": ["number", "null"]},
"taux_max_pct": {"type": ["number", "null"]},
"ltv_min_pct": {"type": ["number", "null"]},
"ltv_max_pct": {"type": ["number", "null"]},
"duree_max_ans": {"type": ["integer", "null"]},
"specialites": {"type": "array", "items": {"type": "string"}},
"devises": {"type": "array", "minItems": 1, "items": {"type": "string"}}
}
}
},
"sections": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "titre", "ordre", "gating"],
"properties": {
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
"titre": {"type": "string", "minLength": 1},
"ordre": {"type": "integer", "minimum": 1},
"gating": {"type": "boolean"}
}
}
},
"documents": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "label", "categorie", "applies_to", "is_required"],
"properties": {
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
"label": {"type": "string", "minLength": 1},
"categorie": {"type": "string", "enum": ["client", "immobilier", "etrangers"]},
"applies_to": {"type": "string", "enum": ["all", "resident_rd", "etranger"]},
"is_required": {"type": "boolean"}
}
}
},
"autorisations": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "label", "base_legale", "signature_method"],
"properties": {
"id": {"type": "string", "pattern": "^[a-z0-9_]+$"},
"label": {"type": "string", "minLength": 1},
"base_legale": {"type": "string", "minLength": 1},
"signature_method": {"type": "string", "minLength": 1}
}
}
},
"gate_spec": {
"type": "object",
"required": ["endpoint", "submit_endpoint", "condition_keys", "conditions", "banniere", "role_validation", "workflow_suivi_banque"],
"properties": {
"condition_keys": {"type": "array", "minItems": 4, "maxItems": 4, "items": {"type": "string"}},
"conditions": {
"type": "array",
"minItems": 4,
"maxItems": 4,
"items": {
"type": "object",
"required": ["key", "ordre", "label"],
"properties": {
"key": {"type": "string", "minLength": 1},
"ordre": {"type": "integer", "minimum": 1},
"label": {"type": "string", "minLength": 1}
}
}
},
"banniere": {
"type": "object",
"required": ["niveau", "persistante", "titre", "lignes", "consequence"],
"properties": {
"niveau": {"const": "critique"},
"persistante": {"type": "boolean"},
"titre": {"type": "string", "minLength": 1},
"lignes": {"type": "array", "minItems": 4, "maxItems": 4, "items": {"type": "string"}},
"consequence": {"type": "string", "minLength": 1}
}
},
"role_validation": {
"type": "object",
"required": ["role_id", "erpnext_role_name"],
"properties": {
"role_id": {"type": "string", "minLength": 1},
"erpnext_role_name": {"type": "string", "minLength": 1}
}
},
"workflow_suivi_banque": {"type": "array", "minItems": 1, "items": {"type": "string"}}
}
},
"gate_status_example": {
"type": "object",
"required": ["dossier_id", "gate_status", "can_submit", "reasons"],
"properties": {
"dossier_id": {"type": "string", "minLength": 1},
"can_submit": {"type": "boolean"},
"reasons": {"type": "array", "items": {"type": "string"}},
"gate_status": {
"type": "object",
"required": ["can_submit", "conditions_total", "conditions_completed", "overall_percent", "conditions"],
"properties": {
"can_submit": {"type": "boolean"},
"conditions_total": {"type": "integer", "minimum": 4, "maximum": 4},
"conditions_completed": {"type": "integer", "minimum": 0, "maximum": 4},
"overall_percent": {"type": "number", "minimum": 0, "maximum": 100}
}
}
}
}
}
}
@@ -0,0 +1,313 @@
#!/usr/bin/env python3
"""Générateur du module Financement Bancaire — parcours hypothécaire RD.
Sprint 5 · CRM natif · roadmap L52 (« workflow vente end-to-end » — volet
financement) + DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md (Michel).
Transforme le contrat `financement_spec.json` en artefacts prêts à consommer
par le frontend `renderHypotheque` et le backend `/api/hypotheque/*` :
- banques.json → profils des banques partenaires (taux/LTV/durée)
- sections.json → structure accordéon (apport → info → … → envoi)
- documents.json → catalogue des documents exigés par catégorie
- autorisations.json → autorisations à signer (OTO Sign)
- gate_spec.json → gate check 4 conditions + bannière critique
- gate_status_example.json → exemple CALCULÉ (dossier bloqué) auditable
- MANIFEST.json → traçabilité + comptes dérivés
Le CŒUR métier (calcul d'apport 20/30 + gate check 4 conditions) vit dans
`finlib/gate.py` (fonctions pures, testées). Ce fichier ne fait qu'orchestrer :
charger le contrat, assembler le bundle, valider (schéma + invariants), écrire.
Anti-invention (#6) : aucune valeur (taux d'apport, documents, autorisations,
noms de rôle) n'est codée en dur — tout vient du contrat + du contrat RBAC. Ce
worker n'écrit JAMAIS sur le VPS (l'application réelle est côté serveur).
Sortie déterministe (tri stable, aucun horodatage) → diffable + re-générable.
Sous-commandes :
build [-o OUT] → écrit les 7 fichiers d'artefacts
validate → (re)génère en mémoire, valide schéma + 12 invariants ;
sort en erreur si un invariant casse (anti-régression).
"""
from __future__ import annotations
import argparse
import json
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", ".."))
sys.path.insert(0, _HERE)
# Réutilisation (#5 : zéro duplication) du validateur JSON-Schema maison Publiciste.
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))
from finlib import builder, gate # noqa: E402
from finlib.rbac import RoleResolver # noqa: E402
from lib import validator as maison # type: ignore # noqa: E402
_SPEC_PATH = os.path.join(_HERE, "financement_spec.json")
_SCHEMA_PATH = os.path.join(_HERE, "financement.schema.json")
_DEFAULT_OUT = os.path.join(_HERE, "out")
# Fichiers écrits par `build` (nom → clé du bundle).
_ARTIFACTS = [
("banques.json", "banques"),
("sections.json", "sections"),
("documents.json", "documents"),
("autorisations.json", "autorisations"),
("gate_spec.json", "gate_spec"),
("gate_status_example.json", "gate_status_example"),
("MANIFEST.json", "manifest"),
]
def _eprint(*args) -> None:
print(*args, file=sys.stderr)
def _load(path: str) -> dict:
with open(path, encoding="utf-8") as fh:
return json.load(fh)
def _write_json(path: str, data) -> None:
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
fh.write("\n")
def _build_bundle() -> tuple[dict, dict]:
spec = _load(_SPEC_PATH)
resolver = RoleResolver.from_path()
return spec, builder.build_bundle(spec, resolver)
def _dups(ids: list[str]) -> list[str]:
seen: set[str] = set()
dups: set[str] = set()
for i in ids:
if i in seen:
dups.add(i)
seen.add(i)
return sorted(dups)
def _validate_bundle(spec: dict, bundle: dict) -> list[str]:
"""Schéma de sortie + 12 invariants métier du module Financement Bancaire."""
schema = _load(_SCHEMA_PATH)
errors = list(maison.validate(bundle, schema))
banques = bundle["banques"]
sections = bundle["sections"]
documents = bundle["documents"]
autorisations = bundle["autorisations"]
gate_spec = bundle["gate_spec"]
manifest = bundle["manifest"]
cfg = gate.GateConfig.from_spec(spec)
# Invariant 1 · unicité des identifiants (banques, documents, autorisations, sections).
for coll_name, coll in (
("banques", banques),
("documents", documents),
("autorisations", autorisations),
("sections", sections),
):
d = _dups([x["id"] for x in coll])
if d:
errors.append(f"Identifiant dupliqué dans {coll_name} : {d}")
# Invariant 2 · taux d'apport : 0 < résident < étranger ≤ 100 (amendement Michel).
taux = cfg.taux_apport_pct
tr = taux.get("resident_rd")
te = taux.get("etranger")
if tr is None or te is None:
errors.append("Taux d'apport résident/étranger manquant dans le contrat.")
elif not (0 < tr < te <= 100):
errors.append(
f"Taux d'apport invalides : résident={tr}, étranger={te} "
f"(attendu 0 < résident < étranger ≤ 100)."
)
# Invariant 3 · fourchettes banque cohérentes (min ≤ max quand les deux présents).
for b in banques:
for lo, hi, nom in (
("taux_min_pct", "taux_max_pct", "taux"),
("ltv_min_pct", "ltv_max_pct", "LTV"),
):
a, z = b.get(lo), b.get(hi)
if a is not None and z is not None and a > z:
errors.append(f"Banque {b['id']!r} : {nom} min {a} > max {z}.")
# Invariant 4 · rôles du contrat résolus dans RBAC (anti-invention #6).
resolver = RoleResolver.from_path()
for cle, rid in spec["roles"].items():
try:
resolver.erpnext_name(rid)
except (KeyError, ValueError) as exc:
errors.append(f"Rôle {cle}={rid!r} irrésoluble dans RBAC : {exc}")
# Invariant 5 · sections : `ordre` contigu 1..N, unique.
ordres = [s["ordre"] for s in sections]
if sorted(ordres) != list(range(1, len(sections) + 1)):
errors.append(f"Ordres de sections non contigus 1..N : {sorted(ordres)}")
# Invariant 6 · exactement UNE section `gating`, en position 1 (apport_initial).
gating = [s for s in sections if s.get("gating")]
if len(gating) != 1:
errors.append(f"Attendu exactement 1 section gating, trouvé {len(gating)}.")
elif gating[0]["ordre"] != 1 or gating[0]["id"] != "apport_initial":
errors.append(
"La section gating doit être `apport_initial` en position 1 "
f"(trouvé id={gating[0]['id']!r}, ordre={gating[0]['ordre']})."
)
# Invariant 7 · gate : exactement 4 conditions, clés == CONDITION_KEYS, ordre 1..4.
conds = gate_spec["conditions"]
keys = [c["key"] for c in conds]
if set(keys) != set(gate.CONDITION_KEYS):
errors.append(
f"Clés de condition {sorted(set(keys))} ≠ clés canoniques "
f"{sorted(gate.CONDITION_KEYS)}."
)
if [c["ordre"] for c in conds] != [1, 2, 3, 4]:
errors.append(f"Ordres des conditions du gate ≠ 1..4 : {[c['ordre'] for c in conds]}.")
if gate_spec["condition_keys"] != list(gate.CONDITION_KEYS):
errors.append("gate_spec.condition_keys ≠ ordre canonique CONDITION_KEYS.")
# Invariant 8 · bannière : 4 lignes, alignées 1-pour-1 sur les 4 conditions.
lignes = gate_spec["banniere"]["lignes"]
if len(lignes) != len(gate.CONDITION_KEYS):
errors.append(
f"Bannière : {len(lignes)} lignes ≠ {len(gate.CONDITION_KEYS)} conditions."
)
# Invariant 9 · documents « étrangers » exclusifs + cohérence categorie⟺applies_to.
req_res = set(gate.required_document_ids("resident_rd", cfg))
req_etr = set(gate.required_document_ids("etranger", cfg))
if not req_res < req_etr:
errors.append(
"Les documents requis résident doivent être un sous-ensemble STRICT "
"des documents requis étranger (les pièces étrangers ajoutent des exigences)."
)
for d in documents:
etr_cat = d["categorie"] == "etrangers"
etr_app = d["applies_to"] == "etranger"
if etr_cat != etr_app:
errors.append(
f"Document {d['id']!r} : categorie/applies_to incohérents "
f"(categorie={d['categorie']}, applies_to={d['applies_to']})."
)
# Invariant 10 · statuts recevables ⊆ statuts déclarés.
statuses = set(spec["document_statuses"])
recevables = set(spec["document_statuses_recevables"])
if not recevables <= statuses:
errors.append(
f"document_statuses_recevables {sorted(recevables)}"
f"document_statuses {sorted(statuses)}."
)
# Invariant 11 · exemple de gate CALCULÉ cohérent (auto-audit du cœur métier).
ex = bundle["gate_status_example"]
gs = ex["gate_status"]
recomputed_can = gate.gate_status(spec["dossier_exemple"], cfg)["can_submit"]
if ex["can_submit"] != recomputed_can or gs["can_submit"] != recomputed_can:
errors.append("gate_status_example.can_submit incohérent avec le recalcul.")
if bool(ex["reasons"]) == ex["can_submit"]:
errors.append("Incohérence : `reasons` non vide DOIT équivaloir à ¬can_submit.")
completed = sum(1 for k in gate.CONDITION_KEYS if gs["conditions"][k]["ok"])
if gs["conditions_completed"] != completed:
errors.append("gate_status_example.conditions_completed mal compté.")
# Invariant 12 · cohérence des comptes du manifeste avec les collections.
c = manifest["counts"]
expected = {
"banques": len(banques),
"sections": len(sections),
"documents": len(documents),
"documents_requis_resident": len(req_res),
"documents_requis_etranger": len(req_etr),
"autorisations": len(autorisations),
"gate_conditions": len(conds),
"banniere_lignes": len(lignes),
"workflow_suivi_etapes": len(gate_spec["workflow_suivi_banque"]),
}
for k, v in expected.items():
if c.get(k) != v:
errors.append(f"manifest.counts.{k}={c.get(k)}{v} (dérive de compte).")
return errors
def cmd_build(args: argparse.Namespace) -> int:
spec, bundle = _build_bundle()
errors = _validate_bundle(spec, bundle)
if errors:
_eprint("❌ Bundle invalide — génération refusée (anti-régression) :")
for e in errors:
_eprint(f" - {e}")
return 1
out = os.path.abspath(args.out)
os.makedirs(out, exist_ok=True)
for fname, key in _ARTIFACTS:
_write_json(os.path.join(out, fname), bundle[key])
m = bundle["manifest"]
c = m["counts"]
print(f"✅ Artefacts Financement Bancaire générés dans {out}")
print(f" banques.json : {c['banques']} banques partenaires")
print(f" sections.json : {c['sections']} sections (accordéon)")
print(
f" documents.json : {c['documents']} documents "
f"(requis résident {c['documents_requis_resident']} · étranger {c['documents_requis_etranger']})"
)
print(f" autorisations.json : {c['autorisations']} autorisations (OTO Sign)")
print(f" gate_spec.json : {c['gate_conditions']} conditions + bannière critique")
ex = bundle["gate_status_example"]
print(
f" gate_status_example: dossier {ex['dossier_id']} · "
f"can_submit={ex['can_submit']} · {len(ex['reasons'])} raison(s) de blocage"
)
return 0
def cmd_validate(args: argparse.Namespace) -> int:
spec, bundle = _build_bundle()
errors = _validate_bundle(spec, bundle)
if errors:
_eprint("❌ Validation KO :")
for e in errors:
_eprint(f" - {e}")
return 1
c = bundle["manifest"]["counts"]
print(
f"✅ Validation OK — {c['banques']} banques, {c['sections']} sections, "
f"{c['documents']} documents, {c['autorisations']} autorisations, "
f"gate 4 conditions, schéma + 12 invariants verts."
)
return 0
def main(argv: list[str] | None = None) -> int:
p = argparse.ArgumentParser(
description="Générateur du module Financement Bancaire (parcours hypothécaire RD)."
)
sub = p.add_subparsers(dest="cmd", required=True)
pb = sub.add_parser("build", help="génère les 7 artefacts out/")
pb.add_argument("-o", "--out", default=_DEFAULT_OUT, help="dossier de sortie (défaut: ./out)")
pb.set_defaults(func=cmd_build)
pv = sub.add_parser("validate", help="valide le bundle (schéma + 12 invariants) sans écrire")
pv.set_defaults(func=cmd_validate)
args = p.parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,215 @@
{
"version": "1.0.0",
"module": "financement_bancaire",
"titre": "Financement Bancaire — parcours hypothécaire immobilier RD",
"source_directive": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md",
"roles": {
"conseiller_wag": "ventes-conseiller",
"validation_dossier": "ventes-conseiller",
"escalade": "ventes-directeur",
"tresorerie_apport": "compta-tresorier"
},
"apport": {
"note": "Apport initial minimum exigé AVANT toute démarche bancaire (amendement Michel 2026-08-03). Source des taux : la DIRECTIVE (§1 plan de paiement + amendement) — jamais un taux inventé (#6). Ce sont aussi des exigences des banques dominicaines (Ley 189-11).",
"residence_types": [
{
"code": "resident_rd",
"label": "Résident RD",
"taux_apport_pct": 20,
"source": "DIRECTIVE §1 + amendement"
},
{
"code": "etranger",
"label": "Étranger",
"taux_apport_pct": 30,
"source": "DIRECTIVE §1 + amendement"
}
]
},
"document_statuses": ["Non déposé", "Déposé", "Validé WAG", "Envoyé banque"],
"document_statuses_recevables": ["Validé WAG", "Envoyé banque"],
"banques": [
{
"id": "banreservas",
"nom": "Banreservas",
"type": "banque d'État",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": 80,
"duree_max_ans": 25,
"specialites": ["residents", "taux_preferentiels"],
"devises": ["DOP"],
"note": "Taux préférentiels résidents. Taux exacts à confirmer (relationship manager · #6)."
},
{
"id": "popular",
"nom": "Banco Popular Dominicano",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": 75,
"ltv_max_pct": 85,
"duree_max_ans": null,
"specialites": ["reseau_large", "rapide", "confotur"],
"devises": ["DOP"],
"note": "Réseau large, CONFOTUR-friendly. Taux et durée à confirmer (#6)."
},
{
"id": "bhd_leon",
"nom": "BHD León",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": 30,
"specialites": ["haut_de_gamme", "etrangers", "bilingue"],
"devises": ["DOP", "USD"],
"note": "Haut de gamme, étrangers acceptés, bilingue. Taux/LTV à confirmer (#6)."
},
{
"id": "santa_cruz",
"nom": "Banco Santa Cruz",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": null,
"specialites": ["taux_competitifs", "prequalif_rapide"],
"devises": ["DOP"],
"note": "Taux compétitifs, pré-qualif rapide. Chiffres à confirmer (#6)."
},
{
"id": "scotiabank_rd",
"nom": "Scotiabank República Dominicana",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": 70,
"duree_max_ans": null,
"specialites": ["etrangers", "devise_usd"],
"devises": ["DOP", "USD"],
"note": "Spécialistes étrangers, devise USD acceptée. Taux/durée à confirmer (#6)."
},
{
"id": "lopez_de_haro",
"nom": "López de Haro",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": null,
"specialites": ["haut_de_gamme", "niche"],
"devises": ["DOP"],
"note": "Niche haut de gamme. Profil complet à confirmer (#6)."
}
],
"documents": [
{"id": "cedula_passeport", "label": "Cédula ou Passeport (2 côtés)", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "preuve_adresse", "label": "Preuve d'adresse récente (<3 mois)", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "fiches_paye_3", "label": "3 dernières fiches de paye ou déclarations ITBIS/DGII", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "etats_bancaires_6m", "label": "États bancaires 6 derniers mois", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "lettre_employeur", "label": "Lettre employeur ou attestation profession libérale", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "certif_solvabilite", "label": "Certification de solvabilité", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "certif_datacredito", "label": "Certification DataCrédito (délivrée par la banque)", "categorie": "client", "applies_to": "all", "is_required": true},
{"id": "promesa_notariee", "label": "Promesa Irrévocable signée notariée", "categorie": "immobilier", "applies_to": "all", "is_required": true},
{"id": "fideicomiso", "label": "Fideicomiso Inmobiliario constitué (Ley 189-11)", "categorie": "immobilier", "applies_to": "all", "is_required": true},
{"id": "titre_definitif", "label": "Titre définitif ou Certificado de Título (si disponible)", "categorie": "immobilier", "applies_to": "all", "is_required": false},
{"id": "passeport_2cotes", "label": "Passeport valide (2 côtés)", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
{"id": "visa_residence", "label": "Visa / résidence si applicable", "categorie": "etrangers", "applies_to": "etranger", "is_required": false},
{"id": "lettre_origine_fonds", "label": "Lettre origine des fonds notariée (FinCEN GTO)", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
{"id": "refs_bancaires_intl", "label": "Références bancaires internationales", "categorie": "etrangers", "applies_to": "etranger", "is_required": true},
{"id": "refs_perso", "label": "Références commerciales / personnelles", "categorie": "etrangers", "applies_to": "etranger", "is_required": true}
],
"autorisations": [
{"id": "partage_donnees", "label": "Autorisation de partage de données personnelles avec la banque", "base_legale": "RGPD / Ley 172-13", "signature_method": "OTO Sign"},
{"id": "investigation_datacredito", "label": "Autorisation d'investigation DataCrédito", "base_legale": "Ley 172-13", "signature_method": "OTO Sign"},
{"id": "mandat_wag", "label": "Mandat WAG comme intermédiaire de financement", "base_legale": "mandat contractuel", "signature_method": "OTO Sign"},
{"id": "acknowledgment_risques", "label": "Acknowledgment — risques de refus bancaire + politique annulation", "base_legale": "consentement éclairé", "signature_method": "OTO Sign"}
],
"formulaires_types": [
"Solicitud de Préstamo Hipotecario",
"Autorización de investigación crediticia (DataCrédito)",
"Declaración jurada de patrimonio",
"Autorización débito automático quote-part",
"Certificación de origen de fondos (étrangers)"
],
"sections": [
{"id": "apport_initial", "titre": "Statut apport initial", "ordre": 1, "gating": true, "resume": "Prix unité · statut résidence · apport requis (20%/30%) · apport versé · solde restant · barre 0-100%."},
{"id": "info_achat", "titre": "Information de l'achat", "ordre": 2, "gating": false, "resume": "Résumé auto de la réservation : projet, unité, plan de paiement, apport, solde à financer, statuts Fideicomiso + CONFOTUR."},
{"id": "choix_banque", "titre": "Choix de la banque partenaire", "ordre": 3, "gating": false, "resume": "Sélection d'UNE banque prioritaire (radio) + multi-dossiers possibles ; profils taux/LTV/durée/spécialités."},
{"id": "formulaires", "titre": "Formulaires banque pré-remplis", "ordre": 4, "gating": false, "resume": "Formulaires officiels PDF pré-remplis, téléchargés, signés, ré-uploadés ; journal audit des signatures."},
{"id": "exigences", "titre": "Exigences bancaires par banque", "ordre": 5, "gating": false, "resume": "Liste des documents exigés cochée au fil des uploads ; statut par item (Non déposé / Déposé / Validé WAG / Envoyé banque)."},
{"id": "autorisations", "titre": "Autorisations à signer", "ordre": 6, "gating": false, "resume": "Autorisations à signer AVANT envoi (OTO Sign horodaté + IP + hash)."},
{"id": "envoi", "titre": "Envoi à la banque", "ordre": 7, "gating": false, "resume": "Bouton « Envoyer à la banque » (gate 4 conditions) + timeline suivi banque + statut temps réel."}
],
"gate": {
"endpoint": "GET /api/hypotheque/dossier/{id}/gate-status",
"submit_endpoint": "POST /api/hypotheque/dossier/{id}/submit",
"conditions": [
{"key": "apport_initial_complet", "ordre": 1, "label": "Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié"},
{"key": "documents_exiges", "ordre": 2, "label": "TOUS les documents exigés déposés et validés WAG"},
{"key": "autorisations_signees", "ordre": 3, "label": "TOUTES les autorisations signées électroniquement"},
{"key": "validation_wag", "ordre": 4, "label": "Validation manuelle WAG confirmée par le conseiller référent"}
],
"banniere": {
"niveau": "critique",
"couleurs": ["rouge", "or"],
"persistante": true,
"titre": "⚠ AUCUN DOCUMENT N'EST TRANSMIS À LA BANQUE tant que :",
"lignes": [
"Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié",
"TOUS les documents exigés ne sont pas déposés dans le portail",
"TOUTES les autorisations ne sont pas signées électroniquement",
"Validation manuelle WAG confirmée par le conseiller référent"
],
"consequence": "Le bouton « Envoyer à la banque » reste désactivé tant que ces 4 conditions ne sont pas remplies. Progression affichée en temps réel avec barre 0-100% par condition."
}
},
"workflow_suivi_banque": [
"Prequalifié",
"Dossier soumis",
"Analyse crédit",
"Offre reçue",
"Contrat signé",
"Décaissement"
],
"dossier_exemple": {
"id": "DEMO-P07-A312",
"projet": "P07 Aqua Terra Las Terrenas",
"unite": "A-312",
"prix_usd": 145000,
"residence_type": "etranger",
"banque_choisie": "scotiabank_rd",
"paiements": [
{"date": "2026-06-15", "montant_verse_usd": 15000, "moyen": "Cardnet"},
{"date": "2026-07-20", "montant_verse_usd": 14500, "moyen": "Cardnet"}
],
"documents": [
{"id": "cedula_passeport", "statut": "Validé WAG"},
{"id": "preuve_adresse", "statut": "Validé WAG"},
{"id": "fiches_paye_3", "statut": "Déposé"},
{"id": "etats_bancaires_6m", "statut": "Validé WAG"},
{"id": "lettre_employeur", "statut": "Non déposé"},
{"id": "certif_solvabilite", "statut": "Non déposé"},
{"id": "certif_datacredito", "statut": "Non déposé"},
{"id": "promesa_notariee", "statut": "Validé WAG"},
{"id": "fideicomiso", "statut": "Déposé"},
{"id": "passeport_2cotes", "statut": "Validé WAG"},
{"id": "lettre_origine_fonds", "statut": "Non déposé"},
{"id": "refs_bancaires_intl", "statut": "Non déposé"},
{"id": "refs_perso", "statut": "Non déposé"}
],
"autorisations": [
{"id": "partage_donnees", "signature_date": "2026-07-21"},
{"id": "investigation_datacredito", "signature_date": "2026-07-21"},
{"id": "mandat_wag", "signature_date": null},
{"id": "acknowledgment_risques", "signature_date": null}
],
"wag_validated_by": null
}
}
@@ -0,0 +1,7 @@
"""finlib · logique du module Financement Bancaire (parcours hypothécaire RD).
Sous-modules :
rbac — résolution des rôles depuis le contrat RBAC 50 rôles (zéro invention).
gate — calcul de l'apport + gate check 4 conditions (cœur métier, pur).
builder — assemblage du bundle d'artefacts déterministe depuis le contrat.
"""
@@ -0,0 +1,100 @@
"""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"]),
},
"roles_rbac_utilises": [roles_resolved[k] for k in sorted(roles_resolved)],
}
return {
"manifest": manifest,
"banques": banques,
"sections": sections,
"documents": documents,
"autorisations": autorisations,
"gate_spec": gate_spec,
"gate_status_example": gate_status_example,
}
@@ -0,0 +1,201 @@
"""Cœur métier du module Financement Bancaire : apport initial + gate check.
Fonctions PURES (aucune I/O, aucun horodatage) → testables et déterministes.
Elles matérialisent l'exigence Michel (DIRECTIVE + amendement 2026-08-03) :
AUCUN dossier n'est transmis à la banque tant que les 4 conditions ne sont pas
remplies :
1. apport_initial_complet — dépôt initial (20 % résident RD · 30 % étranger)
intégralement versé et vérifié ;
2. documents_exiges — tous les documents `is_required` de la banque
choisie au statut « Validé WAG » ou « Envoyé banque » ;
3. autorisations_signees — toutes les autorisations signées (date non nulle) ;
4. validation_wag — un conseiller WAG a validé (`wag_validated_by`).
Anti-invention (#6) : les taux d'apport (20/30) et la liste des documents/
autorisations ne sont JAMAIS codés en dur ici — ils proviennent du contrat
`financement_spec.json` via `GateConfig`. Changer le contrat change le gate,
sans toucher ce code.
"""
from __future__ import annotations
from typing import Any
# Ordre canonique des 4 clés de condition du gate (= ordre du contrat / bannière).
CONDITION_KEYS = (
"apport_initial_complet",
"documents_exiges",
"autorisations_signees",
"validation_wag",
)
class GateConfig:
"""Vue en lecture du contrat, indexée pour le calcul du gate."""
def __init__(self, spec: dict) -> None:
self.taux_apport_pct: dict[str, float] = {
rt["code"]: float(rt["taux_apport_pct"])
for rt in spec["apport"]["residence_types"]
}
self.documents: list[dict] = list(spec["documents"])
self.autorisations: list[dict] = list(spec["autorisations"])
self.statuts_recevables: set[str] = set(spec["document_statuses_recevables"])
self.banques: dict[str, dict] = {b["id"]: b for b in spec["banques"]}
@classmethod
def from_spec(cls, spec: dict) -> "GateConfig":
return cls(spec)
def _round1(x: float) -> float:
"""Arrondi à 1 décimale, stable pour un artefact diffable."""
return round(x + 0.0, 1)
def apport_requis_usd(prix_usd: float, residence_type: str, cfg: GateConfig) -> float:
"""Apport requis = prix × taux (20 % résident RD, 30 % étranger)."""
if residence_type not in cfg.taux_apport_pct:
raise KeyError(
f"residence_type {residence_type!r} inconnu "
f"(attendu : {sorted(cfg.taux_apport_pct)})."
)
return round(float(prix_usd) * cfg.taux_apport_pct[residence_type] / 100.0, 2)
def apport_verse_usd(dossier: dict) -> float:
"""Somme des versements enregistrés (0 si aucun)."""
total = 0.0
for p in dossier.get("paiements", []):
total += float(p.get("montant_verse_usd", 0) or 0)
return round(total, 2)
def required_document_ids(residence_type: str, cfg: GateConfig) -> list[str]:
"""Documents `is_required` applicables au type de résidence du client.
Un document est exigé si `is_required` ET (`applies_to` == "all" OU
`applies_to` == residence_type). Les documents « étrangers uniquement » ne
s'appliquent donc pas à un résident RD.
"""
out: list[str] = []
for d in cfg.documents:
if not d.get("is_required"):
continue
applies = d.get("applies_to", "all")
if applies in ("all", residence_type):
out.append(d["id"])
return out
def _cond_apport(dossier: dict, cfg: GateConfig) -> dict:
residence = dossier.get("residence_type")
required = apport_requis_usd(dossier.get("prix_usd", 0), residence, cfg)
paid = apport_verse_usd(dossier)
remaining = round(max(0.0, required - paid), 2)
pct = _round1(min(100.0, (paid / required * 100.0) if required > 0 else 100.0))
return {
"ok": paid + 1e-9 >= required,
"required": required,
"paid": paid,
"remaining": remaining,
"percent": pct,
}
def _cond_documents(dossier: dict, cfg: GateConfig) -> dict:
residence = dossier.get("residence_type")
required_ids = required_document_ids(residence, cfg)
by_id = {d["id"]: d for d in dossier.get("documents", [])}
deposited = 0
validated = 0
for did in required_ids:
statut = (by_id.get(did) or {}).get("statut", "Non déposé")
if statut != "Non déposé":
deposited += 1
if statut in cfg.statuts_recevables:
validated += 1
total = len(required_ids)
pct = _round1((validated / total * 100.0) if total > 0 else 100.0)
return {
"ok": validated == total,
"total": total,
"deposited": deposited,
"validated": validated,
"percent": pct,
}
def _cond_autorisations(dossier: dict, cfg: GateConfig) -> dict:
required_ids = [a["id"] for a in cfg.autorisations]
by_id = {a["id"]: a for a in dossier.get("autorisations", [])}
signed = 0
for aid in required_ids:
if (by_id.get(aid) or {}).get("signature_date"):
signed += 1
total = len(required_ids)
pct = _round1((signed / total * 100.0) if total > 0 else 100.0)
return {"ok": signed == total, "total": total, "signed": signed, "percent": pct}
def _cond_validation_wag(dossier: dict) -> dict:
validated_by = dossier.get("wag_validated_by")
ok = bool(validated_by)
return {"ok": ok, "validated_by": validated_by, "percent": 100.0 if ok else 0.0}
def gate_status(dossier: dict, cfg: GateConfig) -> dict:
"""État complet du gate — forme consommée par le frontend (barres 0-100 %).
Correspond au contrat de l'endpoint
`GET /api/hypotheque/dossier/{id}/gate-status` (DIRECTIVE amendement).
"""
conditions = {
"apport_initial_complet": _cond_apport(dossier, cfg),
"documents_exiges": _cond_documents(dossier, cfg),
"autorisations_signees": _cond_autorisations(dossier, cfg),
"validation_wag": _cond_validation_wag(dossier),
}
can_submit = all(conditions[k]["ok"] for k in CONDITION_KEYS)
completed = sum(1 for k in CONDITION_KEYS if conditions[k]["ok"])
overall = _round1(
sum(conditions[k]["percent"] for k in CONDITION_KEYS) / len(CONDITION_KEYS)
)
return {
"can_submit": can_submit,
"conditions_total": len(CONDITION_KEYS),
"conditions_completed": completed,
"overall_percent": overall,
"conditions": conditions,
}
def can_submit_dossier(dossier: dict, cfg: GateConfig) -> tuple[bool, list[str]]:
"""(peut_soumettre, raisons_de_blocage) — vide si soumissible.
`submit_to_bank()` doit renvoyer 403 + ces raisons tant que la liste n'est
pas vide. Ordre des raisons = ordre canonique des conditions.
"""
status = gate_status(dossier, cfg)
reasons: list[str] = []
c = status["conditions"]
if not c["apport_initial_complet"]["ok"]:
a = c["apport_initial_complet"]
reasons.append(
f"Apport initial incomplet : {a['paid']:.0f}/{a['required']:.0f} USD versés "
f"(reste {a['remaining']:.0f} USD · {a['percent']}%)."
)
if not c["documents_exiges"]["ok"]:
d = c["documents_exiges"]
reasons.append(
f"Documents exigés incomplets : {d['validated']}/{d['total']} validés WAG."
)
if not c["autorisations_signees"]["ok"]:
s = c["autorisations_signees"]
reasons.append(
f"Autorisations non signées : {s['signed']}/{s['total']} signées."
)
if not c["validation_wag"]["ok"]:
reasons.append("Validation WAG manquante (aucun conseiller référent n'a validé).")
return status["can_submit"], reasons
@@ -0,0 +1,56 @@
"""Résolution des rôles du module Financement Bancaire depuis le contrat RBAC.
Réutilisation (workflow #5 : zéro duplication de VALEUR) de l'unique source de
vérité des rôles ERPNext : `05_deliverables_mvp/rbac/rbac_50_roles.json` (validé
par `rbac.schema.json` dans le job CI `rbac-tests`). Le contrat du module
(`financement_spec.json`) ne cite JAMAIS un nom de rôle Frappe en dur : il
référence l'`id` stable d'un rôle RBAC → ici on résout `id → erpnext_role_name`.
Conséquence anti-invention (#6) : un `role_id` absent du contrat RBAC lève une
erreur (aucun rôle fabriqué), et renommer un rôle côté RBAC se propage
automatiquement au module sans édition manuelle.
"""
from __future__ import annotations
import json
import os
_HERE = os.path.dirname(os.path.abspath(__file__))
_DELIVERABLES = os.path.normpath(os.path.join(_HERE, "..", "..", ".."))
RBAC_CONTRACT_PATH = os.path.join(_DELIVERABLES, "rbac", "rbac_50_roles.json")
class RoleResolver:
"""Table `role_id → erpnext_role_name` construite depuis le contrat RBAC."""
def __init__(self, contract: dict) -> None:
self._by_id: dict[str, dict] = {}
for role in contract.get("roles", []):
rid = role.get("id")
if rid is None:
raise ValueError("Rôle RBAC sans `id` — contrat corrompu.")
if rid in self._by_id:
raise ValueError(f"`id` de rôle RBAC dupliqué : {rid!r}")
self._by_id[rid] = role
@classmethod
def from_path(cls, path: str = RBAC_CONTRACT_PATH) -> "RoleResolver":
with open(path, encoding="utf-8") as fh:
return cls(json.load(fh))
def erpnext_name(self, role_id: str) -> str:
"""`id` RBAC → nom de rôle Frappe. Lève si l'`id` n'existe pas."""
role = self._by_id.get(role_id)
if role is None:
raise KeyError(
f"role_id {role_id!r} introuvable dans rbac_50_roles.json "
f"(aucun rôle inventé · #6)."
)
name = role.get("erpnext_role_name")
if not name:
raise ValueError(f"Rôle {role_id!r} sans `erpnext_role_name`.")
return name
def known_ids(self) -> frozenset[str]:
return frozenset(self._by_id)
@@ -0,0 +1,37 @@
{
"generated_from": "financement_spec.json",
"source_directive": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md",
"rbac_source": "rbac_50_roles.json",
"source_version": "1.0.0",
"module": "financement_bancaire",
"titre": "Financement Bancaire — parcours hypothécaire immobilier RD",
"counts": {
"banques": 6,
"sections": 7,
"documents": 15,
"documents_requis_resident": 9,
"documents_requis_etranger": 13,
"autorisations": 4,
"gate_conditions": 4,
"banniere_lignes": 4,
"workflow_suivi_etapes": 6
},
"roles_rbac_utilises": [
{
"role_id": "ventes-conseiller",
"erpnext_role_name": "OTO Ventes Conseiller"
},
{
"role_id": "ventes-directeur",
"erpnext_role_name": "OTO Ventes Directeur"
},
{
"role_id": "compta-tresorier",
"erpnext_role_name": "OTO Compta Trésorier"
},
{
"role_id": "ventes-conseiller",
"erpnext_role_name": "OTO Ventes Conseiller"
}
]
}
@@ -0,0 +1,26 @@
[
{
"id": "partage_donnees",
"label": "Autorisation de partage de données personnelles avec la banque",
"base_legale": "RGPD / Ley 172-13",
"signature_method": "OTO Sign"
},
{
"id": "investigation_datacredito",
"label": "Autorisation d'investigation DataCrédito",
"base_legale": "Ley 172-13",
"signature_method": "OTO Sign"
},
{
"id": "mandat_wag",
"label": "Mandat WAG comme intermédiaire de financement",
"base_legale": "mandat contractuel",
"signature_method": "OTO Sign"
},
{
"id": "acknowledgment_risques",
"label": "Acknowledgment — risques de refus bancaire + politique annulation",
"base_legale": "consentement éclairé",
"signature_method": "OTO Sign"
}
]
@@ -0,0 +1,114 @@
[
{
"id": "banreservas",
"nom": "Banreservas",
"type": "banque d'État",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": 80,
"duree_max_ans": 25,
"specialites": [
"residents",
"taux_preferentiels"
],
"devises": [
"DOP"
],
"note": "Taux préférentiels résidents. Taux exacts à confirmer (relationship manager · #6)."
},
{
"id": "popular",
"nom": "Banco Popular Dominicano",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": 75,
"ltv_max_pct": 85,
"duree_max_ans": null,
"specialites": [
"reseau_large",
"rapide",
"confotur"
],
"devises": [
"DOP"
],
"note": "Réseau large, CONFOTUR-friendly. Taux et durée à confirmer (#6)."
},
{
"id": "bhd_leon",
"nom": "BHD León",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": 30,
"specialites": [
"haut_de_gamme",
"etrangers",
"bilingue"
],
"devises": [
"DOP",
"USD"
],
"note": "Haut de gamme, étrangers acceptés, bilingue. Taux/LTV à confirmer (#6)."
},
{
"id": "santa_cruz",
"nom": "Banco Santa Cruz",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": null,
"specialites": [
"taux_competitifs",
"prequalif_rapide"
],
"devises": [
"DOP"
],
"note": "Taux compétitifs, pré-qualif rapide. Chiffres à confirmer (#6)."
},
{
"id": "scotiabank_rd",
"nom": "Scotiabank República Dominicana",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": 70,
"duree_max_ans": null,
"specialites": [
"etrangers",
"devise_usd"
],
"devises": [
"DOP",
"USD"
],
"note": "Spécialistes étrangers, devise USD acceptée. Taux/durée à confirmer (#6)."
},
{
"id": "lopez_de_haro",
"nom": "López de Haro",
"type": "banque privée",
"taux_min_pct": null,
"taux_max_pct": null,
"ltv_min_pct": null,
"ltv_max_pct": null,
"duree_max_ans": null,
"specialites": [
"haut_de_gamme",
"niche"
],
"devises": [
"DOP"
],
"note": "Niche haut de gamme. Profil complet à confirmer (#6)."
}
]
@@ -0,0 +1,107 @@
[
{
"id": "cedula_passeport",
"label": "Cédula ou Passeport (2 côtés)",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "preuve_adresse",
"label": "Preuve d'adresse récente (<3 mois)",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "fiches_paye_3",
"label": "3 dernières fiches de paye ou déclarations ITBIS/DGII",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "etats_bancaires_6m",
"label": "États bancaires 6 derniers mois",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "lettre_employeur",
"label": "Lettre employeur ou attestation profession libérale",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "certif_solvabilite",
"label": "Certification de solvabilité",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "certif_datacredito",
"label": "Certification DataCrédito (délivrée par la banque)",
"categorie": "client",
"applies_to": "all",
"is_required": true
},
{
"id": "promesa_notariee",
"label": "Promesa Irrévocable signée notariée",
"categorie": "immobilier",
"applies_to": "all",
"is_required": true
},
{
"id": "fideicomiso",
"label": "Fideicomiso Inmobiliario constitué (Ley 189-11)",
"categorie": "immobilier",
"applies_to": "all",
"is_required": true
},
{
"id": "titre_definitif",
"label": "Titre définitif ou Certificado de Título (si disponible)",
"categorie": "immobilier",
"applies_to": "all",
"is_required": false
},
{
"id": "passeport_2cotes",
"label": "Passeport valide (2 côtés)",
"categorie": "etrangers",
"applies_to": "etranger",
"is_required": true
},
{
"id": "visa_residence",
"label": "Visa / résidence si applicable",
"categorie": "etrangers",
"applies_to": "etranger",
"is_required": false
},
{
"id": "lettre_origine_fonds",
"label": "Lettre origine des fonds notariée (FinCEN GTO)",
"categorie": "etrangers",
"applies_to": "etranger",
"is_required": true
},
{
"id": "refs_bancaires_intl",
"label": "Références bancaires internationales",
"categorie": "etrangers",
"applies_to": "etranger",
"is_required": true
},
{
"id": "refs_perso",
"label": "Références commerciales / personnelles",
"categorie": "etrangers",
"applies_to": "etranger",
"is_required": true
}
]
@@ -0,0 +1,60 @@
{
"endpoint": "GET /api/hypotheque/dossier/{id}/gate-status",
"submit_endpoint": "POST /api/hypotheque/dossier/{id}/submit",
"condition_keys": [
"apport_initial_complet",
"documents_exiges",
"autorisations_signees",
"validation_wag"
],
"conditions": [
{
"key": "apport_initial_complet",
"ordre": 1,
"label": "Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié"
},
{
"key": "documents_exiges",
"ordre": 2,
"label": "TOUS les documents exigés déposés et validés WAG"
},
{
"key": "autorisations_signees",
"ordre": 3,
"label": "TOUTES les autorisations signées électroniquement"
},
{
"key": "validation_wag",
"ordre": 4,
"label": "Validation manuelle WAG confirmée par le conseiller référent"
}
],
"banniere": {
"niveau": "critique",
"couleurs": [
"rouge",
"or"
],
"persistante": true,
"titre": "⚠ AUCUN DOCUMENT N'EST TRANSMIS À LA BANQUE tant que :",
"lignes": [
"Dépôt initial complet (20% résident RD · 30% étranger) versé et vérifié",
"TOUS les documents exigés ne sont pas déposés dans le portail",
"TOUTES les autorisations ne sont pas signées électroniquement",
"Validation manuelle WAG confirmée par le conseiller référent"
],
"consequence": "Le bouton « Envoyer à la banque » reste désactivé tant que ces 4 conditions ne sont pas remplies. Progression affichée en temps réel avec barre 0-100% par condition."
},
"role_validation": {
"role_id": "ventes-conseiller",
"erpnext_role_name": "OTO Ventes Conseiller"
},
"workflow_suivi_banque": [
"Prequalifié",
"Dossier soumis",
"Analyse crédit",
"Offre reçue",
"Contrat signé",
"Décaissement"
]
}
@@ -0,0 +1,46 @@
{
"dossier_id": "DEMO-P07-A312",
"projet": "P07 Aqua Terra Las Terrenas",
"unite": "A-312",
"banque_choisie": "scotiabank_rd",
"gate_status": {
"can_submit": false,
"conditions_total": 4,
"conditions_completed": 0,
"overall_percent": 39.1,
"conditions": {
"apport_initial_complet": {
"ok": false,
"required": 43500.0,
"paid": 29500.0,
"remaining": 14000.0,
"percent": 67.8
},
"documents_exiges": {
"ok": false,
"total": 13,
"deposited": 7,
"validated": 5,
"percent": 38.5
},
"autorisations_signees": {
"ok": false,
"total": 4,
"signed": 2,
"percent": 50.0
},
"validation_wag": {
"ok": false,
"validated_by": null,
"percent": 0.0
}
}
},
"can_submit": false,
"reasons": [
"Apport initial incomplet : 29500/43500 USD versés (reste 14000 USD · 67.8%).",
"Documents exigés incomplets : 5/13 validés WAG.",
"Autorisations non signées : 2/4 signées.",
"Validation WAG manquante (aucun conseiller référent n'a validé)."
]
}
@@ -0,0 +1,51 @@
[
{
"id": "apport_initial",
"titre": "Statut apport initial",
"ordre": 1,
"gating": true,
"resume": "Prix unité · statut résidence · apport requis (20%/30%) · apport versé · solde restant · barre 0-100%."
},
{
"id": "info_achat",
"titre": "Information de l'achat",
"ordre": 2,
"gating": false,
"resume": "Résumé auto de la réservation : projet, unité, plan de paiement, apport, solde à financer, statuts Fideicomiso + CONFOTUR."
},
{
"id": "choix_banque",
"titre": "Choix de la banque partenaire",
"ordre": 3,
"gating": false,
"resume": "Sélection d'UNE banque prioritaire (radio) + multi-dossiers possibles ; profils taux/LTV/durée/spécialités."
},
{
"id": "formulaires",
"titre": "Formulaires banque pré-remplis",
"ordre": 4,
"gating": false,
"resume": "Formulaires officiels PDF pré-remplis, téléchargés, signés, ré-uploadés ; journal audit des signatures."
},
{
"id": "exigences",
"titre": "Exigences bancaires par banque",
"ordre": 5,
"gating": false,
"resume": "Liste des documents exigés cochée au fil des uploads ; statut par item (Non déposé / Déposé / Validé WAG / Envoyé banque)."
},
{
"id": "autorisations",
"titre": "Autorisations à signer",
"ordre": 6,
"gating": false,
"resume": "Autorisations à signer AVANT envoi (OTO Sign horodaté + IP + hash)."
},
{
"id": "envoi",
"titre": "Envoi à la banque",
"ordre": 7,
"gating": false,
"resume": "Bouton « Envoyer à la banque » (gate 4 conditions) + timeline suivi banque + statut temps réel."
}
]
@@ -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()
@@ -79,6 +79,7 @@
"rbac/apply_plan": "rbac-apply", "rbac/apply_plan": "rbac-apply",
"crm/workflow_vente": "workflow-metier", "crm/workflow_vente": "workflow-metier",
"crm/commissions": "workflow-metier", "crm/commissions": "workflow-metier",
"crm/financement_bancaire": "workflow-metier",
"fiscal/ecf_dgii": "workflow-metier", "fiscal/ecf_dgii": "workflow-metier",
"frontend/portails": "frontend", "frontend/portails": "frontend",
"frontend/chat_otoia": "frontend", "frontend/chat_otoia": "frontend",
@@ -5,14 +5,14 @@
"self_module": "devops/deploy_runbook", "self_module": "devops/deploy_runbook",
"counts": { "counts": {
"phases": 7, "phases": 7,
"modules": 22, "modules": 23,
"modules_mapped": 22, "modules_mapped": 23,
"confirmations": 7, "confirmations": 7,
"confirmations_referenced": 7 "confirmations_referenced": 7
}, },
"coverage": { "coverage": {
"ci_modules_count": 22, "ci_modules_count": 23,
"mapped_modules_count": 22, "mapped_modules_count": 23,
"bijective": true, "bijective": true,
"missing_in_map": [], "missing_in_map": [],
"extra_in_map": [], "extra_in_map": [],
@@ -93,6 +93,10 @@
"module": "crm/commissions", "module": "crm/commissions",
"ci_job": "crm-commissions-tests" "ci_job": "crm-commissions-tests"
}, },
{
"module": "crm/financement_bancaire",
"ci_job": "crm-financement-bancaire-tests"
},
{ {
"module": "crm/workflow_vente", "module": "crm/workflow_vente",
"ci_job": "crm-workflow-vente-tests" "ci_job": "crm-workflow-vente-tests"
@@ -79,6 +79,7 @@
"crm/workflow_vente", "crm/workflow_vente",
"crm/dossier_vente", "crm/dossier_vente",
"crm/commissions", "crm/commissions",
"crm/financement_bancaire",
"fiscal/ecf_dgii", "fiscal/ecf_dgii",
"frontend/portails" "frontend/portails"
], ],
@@ -87,6 +88,10 @@
{ {
"item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE", "item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE",
"source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)" "source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)"
},
{
"item": "Financement Bancaire Phase 2 : vrais formulaires PDF officiels des 6 banques + implémentation runtime des endpoints /api/hypotheque/* et de renderHypotheque sur le VPS",
"source": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md (Phase 2 · démarche relationship-manager Michel) · CLAUDE.md #8"
} }
] ]
}, },
@@ -10,13 +10,13 @@
"deliverables_total": 15, "deliverables_total": 15,
"in_repo": 15, "in_repo": 15,
"out_of_scope_full": 0, "out_of_scope_full": 0,
"gated_modules": 22, "gated_modules": 23,
"cited_modules": 22, "cited_modules": 23,
"out_of_scope_items": 12 "out_of_scope_items": 13
}, },
"coverage": { "coverage": {
"ci_modules_count": 22, "ci_modules_count": 23,
"cited_modules_count": 22, "cited_modules_count": 23,
"bijective": true, "bijective": true,
"missing_in_matrix": [], "missing_in_matrix": [],
"phantom_evidence": [], "phantom_evidence": [],
@@ -71,6 +71,7 @@
"expected": [ "expected": [
"crm/commissions", "crm/commissions",
"crm/dossier_vente", "crm/dossier_vente",
"crm/financement_bancaire",
"crm/workflow_vente", "crm/workflow_vente",
"fiscal/ecf_dgii", "fiscal/ecf_dgii",
"frontend/portails" "frontend/portails"
@@ -78,6 +79,7 @@
"cited": [ "cited": [
"crm/commissions", "crm/commissions",
"crm/dossier_vente", "crm/dossier_vente",
"crm/financement_bancaire",
"crm/workflow_vente", "crm/workflow_vente",
"fiscal/ecf_dgii", "fiscal/ecf_dgii",
"frontend/portails" "frontend/portails"
@@ -130,6 +130,11 @@
"ci_job": "crm-commissions-tests", "ci_job": "crm-commissions-tests",
"gated": true "gated": true
}, },
{
"module": "crm/financement_bancaire",
"ci_job": "crm-financement-bancaire-tests",
"gated": true
},
{ {
"module": "fiscal/ecf_dgii", "module": "fiscal/ecf_dgii",
"ci_job": "fiscal-ecf-tests", "ci_job": "fiscal-ecf-tests",
@@ -146,9 +151,13 @@
{ {
"item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE", "item": "création module + import DocTypes/Workflow + transmission e-CF réelle DGII (Compupar) sur ERPNext LIVE",
"source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)" "source": "CLAUDE.md #8 (VPS) · #10 (Cardnet/DGII endpoints prod)"
},
{
"item": "Financement Bancaire Phase 2 : vrais formulaires PDF officiels des 6 banques + implémentation runtime des endpoints /api/hypotheque/* et de renderHypotheque sur le VPS",
"source": "DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md (Phase 2 · démarche relationship-manager Michel) · CLAUDE.md #8"
} }
], ],
"n_modules": 5, "n_modules": 6,
"all_modules_gated": true, "all_modules_gated": true,
"all_artifacts_exist": true, "all_artifacts_exist": true,
"has_evidence": true "has_evidence": true
+1 -1
View File
@@ -65,7 +65,7 @@ Publiciste** (zéro `pip`, runner Gitea sans réseau · CLAUDE.md #2).
## Résultat courant ## Résultat courant
**Verdict `PASS`****22/22** modules à **100/100** (min 100), couverture **Verdict `PASS`****23/23** modules à **100/100** (min 100), couverture
**100 %** des livrables gated. L'audit servira surtout de **gate anti-régression** : **100 %** des livrables gated. L'audit servira surtout de **gate anti-régression** :
retirer des tests, casser un `MANIFEST` ou supprimer un schéma fait chuter le retirer des tests, casser un `MANIFEST` ou supprimer un schéma fait chuter le
module sous 95 → `FAIL`. module sous 95 → `FAIL`.
@@ -4,7 +4,7 @@
"audit": "OTO QA · Audit 4Big Qualité", "audit": "OTO QA · Audit 4Big Qualité",
"version": "1.0", "version": "1.0",
"verdict": "PASS", "verdict": "PASS",
"modules_audited": 22, "modules_audited": 23,
"min_score": 100, "min_score": 100,
"pass_score": 95, "pass_score": 95,
"coverage_ok": true, "coverage_ok": true,
@@ -5,8 +5,8 @@
"pass_score": 95, "pass_score": 95,
"coverage": { "coverage": {
"ok": true, "ok": true,
"ci_modules_count": 22, "ci_modules_count": 23,
"registry_modules_count": 22, "registry_modules_count": 23,
"missing_in_registry": [], "missing_in_registry": [],
"missing_in_ci": [], "missing_in_ci": [],
"not_in_gate": [], "not_in_gate": [],
@@ -550,6 +550,48 @@
} }
] ]
}, },
{
"id": "crm-financement-bancaire",
"path": "crm/financement_bancaire",
"sprint": "S5",
"archetype": "generator",
"applicable_weight": 100,
"earned_weight": 100,
"score": 100,
"verdict": "PASS",
"checks": [
{
"criterion": "DOC",
"weight": 20,
"passed": true,
"evidence": "README.md (6280 octets)"
},
{
"criterion": "CONTRAT",
"weight": 20,
"passed": true,
"evidence": "financement.schema.json"
},
{
"criterion": "TESTS",
"weight": 25,
"passed": true,
"evidence": "35 méthodes test_* dans 1 fichier(s) (seuil 8)"
},
{
"criterion": "CLI",
"weight": 15,
"passed": true,
"evidence": "financement_bancaire_gen.py (argparse + __main__)"
},
{
"criterion": "HANDOFF",
"weight": 20,
"passed": true,
"evidence": "MANIFEST + 6 artefact(s) JSON valides"
}
]
},
{ {
"id": "legal-confotur", "id": "legal-confotur",
"path": "legal/confotur", "path": "legal/confotur",
@@ -930,8 +972,8 @@
} }
], ],
"totals": { "totals": {
"modules": 22, "modules": 23,
"pass": 22, "pass": 23,
"fail": 0, "fail": 0,
"min_score": 100, "min_score": 100,
"max_score": 100, "max_score": 100,
@@ -72,6 +72,7 @@
{ "id": "crm-workflow-vente", "path": "crm/workflow_vente", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · workflow vente lead→CONFOTUR · ci.yml crm-workflow-vente-tests" }, { "id": "crm-workflow-vente", "path": "crm/workflow_vente", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · workflow vente lead→CONFOTUR · ci.yml crm-workflow-vente-tests" },
{ "id": "crm-dossier-vente", "path": "crm/dossier_vente", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · DocType OTO Dossier Vente · ci.yml crm-dossier-vente-tests" }, { "id": "crm-dossier-vente", "path": "crm/dossier_vente", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · DocType OTO Dossier Vente · ci.yml crm-dossier-vente-tests" },
{ "id": "crm-commissions", "path": "crm/commissions", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · barème commissions vendeurs · ci.yml crm-commissions-tests" }, { "id": "crm-commissions", "path": "crm/commissions", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · barème commissions vendeurs · ci.yml crm-commissions-tests" },
{ "id": "crm-financement-bancaire", "path": "crm/financement_bancaire", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · workflow vente end-to-end (volet financement bancaire) · DIRECTIVE_FINANCEMENT_BANCAIRE_COMPLET_20260803.md · ci.yml crm-financement-bancaire-tests" },
{ "id": "fiscal-ecf", "path": "fiscal/ecf_dgii", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · e-CF DGII Compupar · ci.yml fiscal-ecf-tests" }, { "id": "fiscal-ecf", "path": "fiscal/ecf_dgii", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · e-CF DGII Compupar · ci.yml fiscal-ecf-tests" },
{ "id": "frontend-portails", "path": "frontend/portails", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · Workspaces 5 portails rôle · ci.yml frontend-portails-tests" }, { "id": "frontend-portails", "path": "frontend/portails", "sprint": "S4", "archetype": "generator", "source": "roadmap Sprint 4 · Workspaces 5 portails rôle · ci.yml frontend-portails-tests" },
{ "id": "legal-confotur", "path": "legal/confotur", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · DocType CONFOTUR Application · ci.yml legal-confotur-tests" }, { "id": "legal-confotur", "path": "legal/confotur", "sprint": "S5", "archetype": "generator", "source": "roadmap Sprint 5 · DocType CONFOTUR Application · ci.yml legal-confotur-tests" },
@@ -4,8 +4,8 @@
"matrix": "OTO QA · Matrice de régression exhaustive", "matrix": "OTO QA · Matrice de régression exhaustive",
"version": "1.0", "version": "1.0",
"verdict": "PASS", "verdict": "PASS",
"suites": 22, "suites": 23,
"test_methods": 569, "test_methods": 604,
"coverage_ok": true, "coverage_ok": true,
"artifacts": [ "artifacts": [
"regression_plan.json" "regression_plan.json"
@@ -17,8 +17,8 @@
], ],
"coverage": { "coverage": {
"ok": true, "ok": true,
"suites_count": 22, "suites_count": 23,
"gated_in_ci": 23, "gated_in_ci": 24,
"self_module_excluded": "qa/regression", "self_module_excluded": "qa/regression",
"self_module_gated": true, "self_module_gated": true,
"missing_tests_dir": [], "missing_tests_dir": [],
@@ -48,6 +48,17 @@
"test_files": 1, "test_files": 1,
"test_methods": 31 "test_methods": 31
}, },
{
"id": "crm.financement_bancaire",
"path": "crm/financement_bancaire",
"jobs": [
"crm-financement-bancaire-tests"
],
"in_gate": true,
"has_tests_dir": true,
"test_files": 1,
"test_methods": 35
},
{ {
"id": "crm.workflow_vente", "id": "crm.workflow_vente",
"path": "crm/workflow_vente", "path": "crm/workflow_vente",
@@ -271,9 +282,9 @@
], ],
"under_threshold": [], "under_threshold": [],
"totals": { "totals": {
"suites": 22, "suites": 23,
"test_files": 22, "test_files": 23,
"test_methods": 569, "test_methods": 604,
"min_methods": 10, "min_methods": 10,
"under_threshold": 0 "under_threshold": 0
}, },
@@ -27,6 +27,18 @@
"ok": true, "ok": true,
"returncode": 0 "returncode": 0
}, },
{
"id": "crm.financement_bancaire",
"path": "crm/financement_bancaire",
"expected_methods": 35,
"ran": 35,
"passed": 35,
"failures": 0,
"errors": 0,
"skipped": 0,
"ok": true,
"returncode": 0
},
{ {
"id": "crm.workflow_vente", "id": "crm.workflow_vente",
"path": "crm/workflow_vente", "path": "crm/workflow_vente",
@@ -269,11 +281,11 @@
} }
], ],
"totals": { "totals": {
"suites": 22, "suites": 23,
"green": 22, "green": 23,
"red": 0, "red": 0,
"ran": 569, "ran": 604,
"passed": 569, "passed": 604,
"failures": 0, "failures": 0,
"errors": 0, "errors": 0,
"skipped": 0 "skipped": 0
+2 -2
View File
@@ -39,9 +39,9 @@ Transverses : [`devops`](03_agents/devops/AGENT.md) · [`qa`](03_agents/qa/AGENT
## État courant (sourcé) ## État courant (sourcé)
- **Qualité 4Big** : **22/22 modules gated à 100/100** (seuil 95 · `CLAUDE.md` #5), verdict `PASS` - **Qualité 4Big** : **23/23 modules gated à 100/100** (seuil 95 · `CLAUDE.md` #5), verdict `PASS`
— source [`qa/audit_4big/out/quality_report.json`](05_deliverables_mvp/qa/audit_4big/out/quality_report.json). — source [`qa/audit_4big/out/quality_report.json`](05_deliverables_mvp/qa/audit_4big/out/quality_report.json).
- **Régression** : **22 suites gated** couvertes bijectivement vs CI. Le **compte de tests - **Régression** : **23 suites gated** couvertes bijectivement vs CI. Le **compte de tests
agrégé faisant autorité** vit dans [`qa/regression/out/regression_run.json`](05_deliverables_mvp/qa/regression/out/regression_run.json), agrégé faisant autorité** vit dans [`qa/regression/out/regression_run.json`](05_deliverables_mvp/qa/regression/out/regression_run.json),
artefact **commité et byte-gaté** : `ci/check_regression.sh` régénère un `run` frais et exige artefact **commité et byte-gaté** : `ci/check_regression.sh` régénère un `run` frais et exige
l'identité **byte-for-byte** avec la baseline commitée (verdict `PASS`). Recensement déterministe l'identité **byte-for-byte** avec la baseline commitée (verdict `PASS`). Recensement déterministe