Mini-ERP de courtage santé et prévoyance
Un courtier grossiste délégataire reçoit chaque mois des bordereaux, un relevé bancaire et des retours de prestations, et doit en tirer les encaissements, ses commissions et les reversements à ses apporteurs. Ce mini-ERP le fait sur un portefeuille simulé, refuse en entier tout fichier qui ne passe pas les contrôles, alimente un tableau de bord dont chaque mesure est comparée au SQL, pré-remplit les dossiers reçus par un modèle de langage sous validation humaine, et score chaque mois le risque de résiliation.
- Lots refusés en entier, aucune ligne écrite
- 9 sur 27
- Comparaisons entre mesures Power BI et SQL
- 302
- Défauts réintroduits, tous détectés par les tests
- 9 sur 9
- Tests passés
- 255
Contexte et problème
Un courtier grossiste délégataire conçoit des produits d’assurance santé et prévoyance, anime un réseau d’apporteurs et gère, au nom des compagnies, des actes qui reviendraient normalement à l’assureur : quittancement, encaissement, règlement de prestations. Ce que le cabinet a le droit de faire dépend d’une convention de délégation, datée, produit par produit.
Chaque mois, trois courriers financiers arrivent : le bordereau de cotisations de chaque compagnie, le relevé de la banque et le retour de prestations du concentrateur de tiers payant. Avant de facturer quoi que ce soit, le cabinet doit savoir quelles cotisations appelées ont été encaissées, combien les compagnies lui doivent en commission, et combien il reverse à chacun de ses apporteurs. Dans beaucoup de cabinets ce rapprochement se fait encore à la main, et les erreurs se concentrent sur les lignes que personne ne relit : un fichier chargé deux fois, une quittance hors période de garantie, un total qui ne tombe pas juste.
Un contrat, de son côté, change pendant sa vie : de formule, de tarif, d’apporteur. Pour calculer une commission ou régler une prestation trois ans plus tard, il faut pouvoir dire dans quel état il était à une date donnée.
J’ai donc construit le cœur de gestion, la chaîne qui contrôle et rapproche les flux, et le tableau de bord qui en rend compte, sur un portefeuille entièrement simulé. J’y ai ajouté deux modules d’intelligence artificielle : un agent qui lit les documents reçus et pré-remplit un dossier qu’un gestionnaire valide, et une prédiction de résiliation scorée chaque mois. La recherche dans les conditions générales n’est pas faite.
Données
Aucune donnée réelle. Le dépôt produit lui-même son jeu de démonstration, à graine fixe (20261019), avec un
historique arrêté au 30/09/2026. Chaque valeur qui ressemble à un paramètre d’assurance (taxes, taux de
commission, plafonds, formules) est une hypothèse de simulation, listée dans docs/HYPOTHESES.md et stockée
en base avec ses dates de validité, jamais écrite en dur dans le code.
| Entité | Lignes | Source |
|---|---|---|
| Entreprises souscriptrices | 40 | results/generation.json |
| Adhérents | 5 382 | results/generation.json |
| Contrats | 5 737 | results/generation.json |
| Avenants | 28 816 | results/generation.json |
| Appels de cotisation | 51 324 | results/generation.json |
| Encaissements | 37 154 | results/generation.json |
| Prestations | 49 970 | results/generation.json |
| Lignes empreintées au total | 178 658 | results/reproductibilite_sqlite.json |
Le courrier du mois est généré à part : 27 fichiers (bordereaux en CSV et en XLSX, relevés bancaires, retours de prestations), avec un manifeste de vérité terrain qui liste les anomalies injectées, dont un fichier redéposé tel quel, un total faux et un acte non délégué.
Une première version du jeu produisait un ratio de sinistres sur primes supérieur à 100 % sur toutes les périodes (177,67 % en 2024), parce que le générateur ne reliait pas les prestations aux cotisations. Je l’ai corrigé le 22/10 (décision D32 du dépôt) : les demandes de chaque contrat sont calibrées sur sa cotisation, avec une échelle par branche et une consommation à queue lourde, et j’ai remesuré tout ce qui en dépendait. Les prestations de 2020 à 2023 n’ont pas de cotisation en face, le quittancement simulé commençant le 01/10/2023 : aucun ratio ne se lit avant 2024.
Approche
Cinq modules, chacun construit sur les précédents.
Cœur ERP. Un modèle de 33 tables, deux moteurs (PostgreSQL en production, SQLite pour développer et tester) servis par le même code, des migrations Alembic qui montent et redescendent sans résidu. Chaque changement d’un contrat est un avenant daté, et l’état à une date se reconstitue en relisant les avenants : l’API dit de quel avenant provient la valeur retournée. La délégation de gestion est vérifiée avant toute écriture qui engage la compagnie, avec trois périmètres différents dans le jeu, dont une convention arrivée à échéance et non renouvelée. L’interface, en gabarits Jinja2 et HTMX, comprend la fiche contrat, la saisie, l’écran des anomalies et la vue du réseau et des délégations.
Flux financiers. Une chaîne de neuf étapes, chacune lancée par une commande qui renvoie un code de retour : détection des fichiers, archivage avec empreinte SHA-256, chargement, rapprochement, commissions, bordereaux, rapport de détection, journal par lot, alerte. Huit contrôles bloquants s’appliquent avant toute écriture (schéma, fichier déjà chargé, doublon de clé métier, contrat ou adhérent inconnu, total faux, période hors garantie, acte non délégué) : une seule violation rejette le lot entier, et le fichier part avec son rapport d’anomalies. Le graphe Airflow n’importe aucun code métier, il appelle ces commandes. Le rapprochement se fait par référence, puis par montant et date dans une fenêtre paramétrable, et conserve la méthode employée. Les commissions se calculent au taux valide à la date d’échéance de l’appel.
Pilotage. Un modèle en étoile construit par dbt (52 modèles, 151 tests), qui ajoute les cotisations
acquises réparties au prorata des jours, un cliché mensuel du portefeuille et les mouvements d’affaires
nouvelles et de résiliations. Quatre indicateurs sont définis une fois, avec leurs pièges chiffrés, dans
docs/kpi.md : ratio de sinistres sur primes, taux de résiliation, production par apporteur, portefeuille
par branche. Un projet Power BI versionné en texte les affiche, et chaque mesure est comparée à la requête
SQL de contrôle correspondante.


Agent de pré-remplissage. Trente documents fictifs en PDF (demandes de remboursement, factures, attestations d’employeur), dont huit portent une anomalie injectée : nom altéré, adhérent inexistant, total faux, soins antérieurs à l’effet du contrat. Claude Haiku en extrait les champs dans un format structuré imposé, revalidé à la réception. Le dossier est ensuite confronté au portefeuille : adhérent trouvé, nom comparé, contrat rattaché, garantie en vigueur à la date des soins, total égal à la somme des lignes, délégation de la compagnie. Il naît toujours « à valider » : seule la décision du gestionnaire crée la prestation, et la base refuse une prestation liée à un dossier non validé. Chaque réponse du modèle est enregistrée avec l’empreinte de la requête, ce qui rejoue l’évaluation sans clé ni appel payant.

Prédiction de résiliation. Un modèle dbt produit une ligne par contrat et par 1er du mois, avec pour cible une résiliation effective dans les douze mois. Un test dbt vérifie qu’aucune variable ne lit une information postérieure à la date d’observation. L’apprentissage porte sur les 1er janvier 2023 et 2024, le test sur le 1er janvier 2025. Une régression logistique est déployée, LightGBM sert de comparaison, et les deux sont mesurés contre un plafond : la formule même du générateur, que nul modèle ne peut dépasser sur ces données. Un graphe Airflow mensuel score le portefeuille, et la fiche adhérent affiche la probabilité avec ses trois facteurs principaux.

Choix techniques
| Choix | Plutôt que | Pourquoi |
|---|---|---|
| Rejet du lot entier dès qu’un contrôle bloquant échoue | Charger les lignes valides, signaler le reste | Un chargement à moitié fait se découvre des semaines plus tard et ne se défait pas proprement. Le fichier rejeté reste intact dans le dossier des rejets avec son rapport, qui est la pièce à renvoyer à l’émetteur |
| Historisation par avenants, état reconstitué à une date | Une table d’historique en parallèle | L’avenant est déjà l’objet que manipule le métier : l’historique n’est tenu qu’à un endroit, et reconstituer un état revient à le relire. Un contrôle compare le cache porté par la table contrat à cette reconstitution, pour qu’ils ne divergent pas en silence |
| Montants en centimes entiers | Nombres à virgule flottante | Un centime perdu sur un bordereau de commissions est une anomalie comptable, et l’entier se comporte à l’identique sur PostgreSQL et SQLite |
| Un graphe Airflow qui appelle des commandes, sans importer le code métier | Un graphe qui importe l’application | Airflow contraint strictement ses dépendances et l’application utilise SQLAlchemy 2 : les mêler rend l’installation fragile. La chaîne se rejoue aussi à la main sur un poste sans Airflow (courtage flux chaine) |
| Commission au taux valide à la date d’échéance de l’appel | Taux en vigueur au jour du calcul | Une revalorisation du barème réécrirait sinon des bordereaux déjà réglés |
| Contraintes de vérification en base, en plus du code | Validation applicative seule | Un ERP reçoit aussi des écritures par imports et par scripts de reprise. Cinq écritures SQL directes interdites sont refusées par la base sur les deux moteurs |
| Projet Power BI en texte (PBIP) et comparaison de chaque mesure au SQL | Fichier binaire et vérification à l’œil | Un fichier texte se relit et se versionne. La comparaison au SQL vérifie chaque chiffre que le tableau de bord affiche |
| Gabarits Jinja2 et HTMX servis par l’application | Application React séparée | Un écran de gestionnaire est une liste, une fiche et un formulaire. Aucune chaîne de compilation à installer, un seul processus à démarrer pour la démonstration |
Résultats et métriques
Chaque valeur vient d’une exécution enregistrée dans results/. L’intégration continue rejoue les preuves
sur SQLite et sur PostgreSQL et monte la pile Docker ; le dernier passage est réussi (exécution 40685118412). J’ai exécuté le graphe Airflow sur la pile Docker de
mon poste.
| Mesure | Valeur | Source |
|---|---|---|
| Lots reçus, rejetés en entier | 27, dont 9 rejetés, 4 082 lignes écartées, 0 ligne écrite en base | results/flux_rejet_integral.json |
| Fichiers redéposés, état de la base | 27 redéposés, empreinte d’état identique | results/flux_idempotence.json |
| Contrôles bloquants contre les anomalies injectées | 9 sur 9 trouvées, aucun faux positif | results/flux_rapport_detection.json |
| Avertissements contre les anomalies injectées | rappel 0,908, précision 0,649 | results/flux_rapport_detection.json |
| Lignes examinées par le rapprochement | 2 588 : 2 488 rapprochées, 34 partielles, 16 en écart, 24 sans appel, 26 ignorées (acte non délégué) | results/flux_rapprochement.json |
| Commissions calculées, trois mois | 3 704, et 2 978 rétrocessions | results/flux_commissions.json |
| Bordereaux produits | 33, dont 9 de commissions et 24 de rétrocessions | results/flux_bordereaux.json |
| Modèles dbt, tests, exposition | 52 modèles, 151 tests, 3 expositions : 199 réussites, 4 avertissements, 0 erreur | results/dbt_build.txt |
| Mesures Power BI contre SQL | 302 comparaisons, écart relatif maximal 2,9e-14 | results/concordance_dax_sql.csv |
| Migrations qui montent et redescendent | 8 révisions, 33 tables, aucun résidu | results/migrations_postgresql.json |
| Extraction des 30 documents par Claude Haiku | 96,7 % des champs exacts, 8 anomalies sur 8 signalées, aucun document sain bloqué | results/ia_evaluation.json |
| Coût et latence de l’extraction | 0,000272 $ et 1 807 ms en moyenne par document, 0,0081 $ pour les 30 | results/ia_evaluation.json |
| Prédiction de résiliation, test au 01/01/2025 | ROC-AUC 0,645, lift 2,22 sur le premier décile, plafond du générateur 0,662 | results/ml_resiliation.json |
| LightGBM, même test | ROC-AUC 0,645, lift 1,98 : pas mieux que la régression logistique | results/ml_resiliation.json |
| Génération reproductible | même empreinte sur SQLite et PostgreSQL, deux exécutions chacun | results/reproductibilite_postgresql.json |
| Cohérence métier des données | 18 contrôles, tous conformes | results/coherence_metier.json |
| Défauts réintroduits, détectés par les tests | 9 sur 9 | results/mutation.json |
| Suite de tests | 255 passés, 13 sautés | results/pytest.txt |
Les quatre indicateurs, sur le jeu simulé (results/kpi_sql.json) :
| Indicateur | 2024 | 2025 | 2026, 9 mois |
|---|---|---|---|
| Ratio sinistres sur primes | 76,38 % | 79,99 % | 72,07 % |
| Taux de résiliation | 10,98 % | 11,67 % | 8,67 % (11,59 % ramené à l’année) |
La production de 2025 compte 834 affaires nouvelles apportées par dix apporteurs, et le portefeuille au 30/09/2026 compte 3 926 contrats, dont 36 suspendus.


Le résultat qui m’a le plus servi est un défaut : le premier dbt build complet a fait échouer deux tests
de sévérité erreur sur 1 184 appels, parce que le recalcul SQL des commissions ignorait la règle que le code
applique quand la compagnie encaisse elle-même. L’intégration continue n’exécute pas dbt, elle ne l’aurait
pas vu. La règle est désormais reprise dans le SQL et dans la comparaison.

Impact métier
L’outil s’adresse au responsable de gestion d’un cabinet de courtage grossiste délégataire, et à la direction qui lit ses chiffres. Il les aide à trancher deux questions : quel fichier renvoyer à son émetteur avant tout traitement, et quel chiffre croire quand deux calculs d’un même indicateur divergent. Tous les chiffres viennent d’un portefeuille simulé et ne décrivent aucun marché réel.
Mesuré. Sur les 27 fichiers du mois simulé, 9 sont refusés en entier et aucun n’a laissé de ligne en base,
ce que results/flux_rejet_integral.json vérifie lot par lot. Le ratio de sinistres sur primes de 2024 vaut
76,38 % rapporté aux produits dont le cabinet règle les prestations, et 54,25 % rapporté à tous les
produits : prévoyance comprise, qui n’a aucune prestation déléguée, l’indicateur change de plus de vingt points
(results/kpi_sql.json, requête 01b). Avec une seule définition, recoupée en SQL, deux services qui citent
ce ratio parlent du même périmètre.
Non démontré. Aucune durée de rapprochement manuel n’a été chronométrée, et la durée de la chaîne n’est
pas enregistrée dans results/ : je n’avance donc aucun temps gagné. Je n’avance aucun gain en euros non
plus. Les taux, les plafonds et les périmètres de délégation sont des hypothèses de simulation.
Limites et pistes d’amélioration
La recherche dans les conditions générales (module 4b) n’est pas faite. L’extraction est mesurée sur trente documents que j’ai générés, au texte propre : ni scan, ni écriture manuscrite. Les dix champs faux portent tous sur l’émetteur, que le modèle complète d’un libellé de service ; je n’ai pas corrigé la vérité terrain pour les faire passer. La prédiction de résiliation reste modeste (ROC-AUC 0,645) parce que le générateur tire les résiliations au hasard autour de ses propres causes : le plafond calculé avec sa formule n’est que de 0,662. Même dans le décile le plus risqué, 29 % des contrats seulement sont résiliés dans l’année : le score sert à choisir qui un chargé de clientèle appelle en premier. Le remboursement santé est simplifié : il s’applique aux frais présentés, sans base de la Sécurité sociale.
La démonstration en ligne ne montre que l’interface, sur une base SQLite fournie : la chaîne Airflow, dbt et Power BI ne tournent que sur la pile Docker. Hébergée gratuitement, elle s’endort après quinze minutes sans visite, et la première page met alors environ une minute à s’afficher. Comme l’application n’a pas d’authentification, la base de la démonstration est remise à zéro à chaque redémarrage.
Le portefeuille est simulé : les signaux qu’il contient (saisonnalité, hausses du 1er janvier, résiliations liées à l’ancienneté et au tarif) sont ceux que le générateur y a mis. Le rappel et la précision des contrôles se mesurent contre des anomalies que j’ai injectées moi-même, donc ils disent que les contrôles font ce que j’ai écrit qu’ils feraient, pas qu’ils attraperaient les anomalies d’un fichier réel. La précision de 0,649 des avertissements vient surtout d’un code, la proximité du plafond annuel (288 signalements pour 98 anomalies injectées), qui signale aussi des cas que le générateur n’avait pas marqués. Le contrôle des montants atypiques laisse passer 41 anomalies sur 346.
Le bordereau de commissions que la compagnie adresse au cabinet n’est pas ingéré : quand elle encaisse elle-même, le calcul retient la cotisation appelée hors taxe, sur 1 184 appels comptés à part. Le bordereau de prestations émis vers la compagnie n’est pas modélisé. Le jeu couvre la santé individuelle, la santé collective et la prévoyance, ni l’automobile ni les dommages. La capture du graphe Airflow date d’avant la tâche d’alerte : le graphe actuel compte quatorze tâches, la capture en montre treize. Le projet Power BI versionné porte un chemin d’exemple vers les données, à régénérer après un clone.
La suite logique, dans l’ordre : ingérer le bordereau de commissions de la compagnie pour supprimer l’assiette de repli, faire exécuter dbt par l’intégration continue, puis la recherche dans les conditions générales, que je ne publierai qu’avec ses mesures.