Skip to content

Moteur d'éligibilité des entités (hackathon) - #1658

Draft
skelz0r wants to merge 53 commits into
developfrom
hackathon/entity-validation
Draft

Moteur d'éligibilité des entités (hackathon)#1658
skelz0r wants to merge 53 commits into
developfrom
hackathon/entity-validation

Conversation

@skelz0r

@skelz0r skelz0r commented Jun 30, 2026

Copy link
Copy Markdown
Member

Travail hackathon « Validation et filtrage des entités sur DataPass » : un moteur d’éligibilité qui, pour un couple (organisation, démarche), produit un verdict consultatif d’aide à l’instruction — sans décision irréversible, à partir des seules données INSEE déjà connues.

Ce que ça apporte

  • Un moteur extensible (une règle par démarche, résolue par convention de nom).
  • Trois verdicts métier illustrés de bout en bout : commune → valide, menuiserie → invalide, entité publique à caractère commercial (EPIC) → valide à confirmer (zone grise / likely_eligible).
  • Un badge d’éligibilité visible à l’instruction, avec l’explication de la règle appliquée.

Contenu (par PR mergée sur la branche)

Périmètre / suites

  • Le verdict est recalculé à la volée (pas de persistance d’un score — cf. API-7000).
  • Affinage des signaux (SA à capitaux publics, effectif, INPI, JDD administrations API-6998) à poursuivre sur cas réels.

Doc : docs/technique/moteur_eligibilite.md.

Demandes à tester (instruction)

Vue instructeur : /instruction/demandes/:id (connecté en datapass@yopmail.com).
L’auto-instruction est déclenchée à la soumission (SubmitAuthorizationRequestAutoInstructAuthorizationRequest) : la bannière validée / refusée automatiquement s’appuie sur les événements auto_approve / auto_reject historisés, pas sur une déduction état + verdict.

Les ids ci-dessous correspondent à une base fraîchement seedée (rails db:seed) — ils peuvent varier localement ; les intitulés, eux, sont stables (filtrables au tableau de bord).

Id Démarche / cas Verdict Bannière
1024 Aides financières — commune eligible Demande validée automatiquement
1025 Aides financières — EPIC (zone grise) likely_eligible L’organisation semble éligible (à confirmer manuellement)
1026 Aides financières — société privée ineligible Demande refusée automatiquement
1029 HubEE Certificat de décès — commune eligible Demande validée automatiquement

Les demandes API Entreprise sans cas d’usage renvoient un verdict unknown : aucune bannière n’est affichée.

@skelz0r skelz0r self-assigned this Jun 30, 2026
skelz0r and others added 2 commits June 30, 2026 14:50
Bootstrap d'un moteur de règles déterminant l'éligibilité d'une
organisation à un cas d'usage (verdict valide / invalide / likely,
cf. labels « Semble valide / invalide » côté instruction).

EntityEligibility::Engine prend le couple (organization,
authorization_request_form) — et optionnellement la demande pour de
futurs checks pré-soumission, d'où le constructeur .from_request. Il
résout la règle par convention sur l'uid du formulaire
(constantize → unknown si absente) et se passe lui-même comme contexte
à la règle, évitant un objet de contexte dédié.

Premier cas câblé (API-7005) : HubEECertDC éligible si l'organisation
est une commune, via Organization#legal_category existant — la logique
métier reste dans le service, pas sur les attributs bruts du modèle.
…strap-engine

Introduire le moteur d'éligibilité des entités
@skelz0r
skelz0r force-pushed the hackathon/entity-validation branch from 2b6b4a6 to 5a252a7 Compare June 30, 2026 13:07
skelz0r and others added 19 commits June 30, 2026 15:17
Poursuit le bootstrap du moteur d'éligibilité (API-7005) après le cas 1
(Commune → HubEECertDC, déjà mergé) en ajoutant le cas 2 et en
factorisant les règles.

Cas 2 — API Entreprise : EntityEligibility::Rules::APIEntreprise tranche
le seul cas visé par la spec (« entreprise de menuiserie → invalide ») via
le code NAF/APE (activitePrincipaleUniteLegale ∈ 16.23Z, 43.32A, 43.32B) :
ineligible(:menuiserie), sinon unknown. On ne généralise pas tant qu'on
n'a pas d'autres cas réels.

Classe mère EntityEligibility::Rules::Base : porte les outils génériques
(accès organization/authorization_request, builders de verdict générés
depuis Verdict::STATUSES). La lecture des données brutes (code NAF,
catégorie juridique) et les prédicats spécifiques (commune?, menuiserie?)
restent dans la règle concernée, au plus près de leur usage.

Résolution de règle au niveau démarche : l'engine constantize
EntityEligibility::Rules::<classe d'AR démodulisée> au lieu de l'uid du
formulaire, car les cas du ticket raisonnent par démarche (HubEECertDC,
APIEntreprise) et une démarche porte plusieurs formulaires.

Bootstrap d'une doc technique (docs/technique/moteur_eligibilite.md)
décrivant la logique, amenée à itérer.
…entreprise-rule

Câbler l'éligibilité API Entreprise et factoriser les règles
Centralise les lectures brutes du payload INSEE derrière des accesseurs
nommés, source unique pour legal_category et personne_physique?.
La règle ne plonge plus dans insee_payload : elle lit l'accesseur nommé.
Doc du moteur réalignée.
Plus aucune lecture brute du payload INSEE hors Organization.
…ier-une-organisation-sortir-les-bons

Exposer les attributs d'identité d'organisation pour le moteur d'éligibilité
…bilité

Le moteur a besoin de distinguer administration / zone grise (public à
caractère commercial) / autre pour trancher likely_eligible. entity_type
dérive cette typologie du niveau 1 de la catégorie juridique, sans
dupliquer la lecture fine de legal_category.
Première règle à exercer likely_eligible : la zone grise (entités
publiques à caractère commercial, EPIC/SNCF) n'est ni auto-validable ni
refusable, elle relève de la revue humaine. Administration auto-valide,
privé auto-refuse.
…r-de-regles

Exercer likely_eligible via la règle d'éligibilité aide financière
Donne au moteur d'éligibilité une démarche réelle à résoudre : l'Engine
mappe AuthorizationRequest::AideFinanciere sur Rules::AideFinanciere par
convention de nom, ce qui rend le verdict (dont likely_eligible pour la
zone grise) observable de bout en bout dans l'application.
Traduit le statut du moteur en label «Semble valide / invalide» (API-7004)
avec une couleur sémantique DSFR, pour donner à l'instructeur un signal
d'aide à la décision lisible.
Branche le moteur sur la consultation d'une demande : l'instructeur voit
l'estimation d'éligibilité de l'organisation au moment d'instruire.
Donne un jeu de démonstration prêt à l'emploi : une commune (semble
valide), un EPIC en zone grise (à confirmer) et une société privée
(semble invalide), pour observer chaque verdict côté instruction sans
chercher de SIRET réel.
Sous le badge, une ligne décrit le motif (catégorie juridique,
activité…) pour rendre la décision du moteur lisible à l'instruction.
…-navigable-et-badge

Câbler le moteur d'éligibilité de bout en bout (démarche + badge)
@Un3x Un3x changed the title Kickoff Hackathon Moteur d'éligibilité des entités (hackathon) Jun 30, 2026
Un3x added 5 commits June 30, 2026 17:02
Flag de configuration opt-in (défaut: désactivé) pour ne brancher
l'instruction automatique que sur les démarches qui s'y prêtent, sans
toucher au comportement des démarches existantes.
Sur une démarche opt-in, un verdict certain tranche sans humain :
éligible → validation, inéligible → refus motivé. Les cas incertains
(zone grise, indéterminé) restent en revue humaine.
Une fois la demande soumise et persistée, le moteur tranche les
démarches opt-in. Les seeds Aide financière illustrent les trois issues :
commune validée, EPIC en revue, société privée refusée.
…n-selon-eligibilite

Auto-instruire les demandes selon le verdict d'éligibilité
skelz0r and others added 26 commits July 1, 2026 09:33
Le composant Molecules::Instruction::EntityEligibilityVerdictComponent résout
ses libellés (statuts, reasons) via des clés construites dynamiquement
(t(".#{status}"), t(".reasons.#{reason}")). i18n-tasks ne peut pas les tracer
statiquement et les signalait comme inutilisées, rendant spec/i18n_spec.rb rouge
depuis leur introduction. On les déclare dans ignore_unused.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Traduit le EntityEligibility::Verdict en bloc affiché à l'ouverture d'un
formulaire, distinct du badge d'instruction (audience et rendu différents) :

- unknown → render? faux, aucun bloc (l'intro reste inchangée) ;
- eligible / likely_* → encadré léger coloré (icône + texte coloré, la couleur
  n'est jamais seule porteuse de sens : RGAA) ;
- ineligible → prise en charge forte + repli mailto vers le support_email du
  fournisseur.

i18n par convention : namespace entity_eligibility keyé par règle (rule_key
dérivé comme la résolution de l'Engine) avec repli sur `base`. On n'écrit dans la
branche d'une règle que l'override propre à la démarche (ex. la formulation
« menuiserie » d'api_entreprise). Les clés sont résolues dynamiquement, donc
déclarées dans ignore_unused.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
…-7001)

Le contrôleur calcule le verdict en pré-création (sans demande) dès qu'une
organisation courante est présente, et la vue d'intro l'affiche au-dessus des
étapes du formulaire.

En cas d'inéligibilité, le parcours de dépôt est volontairement coupé côté UI :
les étapes sont masquées et le CTA « Débuter ma demande » est remplacé par un
lien de contact du fournisseur. Le moteur reste consultatif (aucun blocage
serveur ici).

Conséquence directe : hubee-cert-dc, réservé aux communes, bloque désormais les
non-communes à l'intro. create_spec ne testait pas l'éligibilité (juste « start
ne persiste pas ») ; on lui donne une organisation commune, prémisse désormais
nécessaire pour atteindre le bouton de démarrage.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Section « Côté demandeur — introduction de formulaire (API-7001) » : calcul du
verdict en pré-création, rendu du bloc par statut, blocage UI en inéligible. Et
la convention i18n par règle avec repli base (namespace entity_eligibility).

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
En bas de l'introduction, on conserve le bouton « Débuter ma demande » mais
désactivé, au lieu de le remplacer par un lien de contact fournisseur. Le repli
de contact reste porté par le bloc d'éligibilité lui-même : la barre d'action
garde ainsi une mise en page stable, l'action attendue restant visible mais
inaccessible.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
API Entreprise est une démarche à ~20 formulaires (cas d'usage) : viser
un cas d'usage précis sans toucher aux autres imposait de descendre la
granularité de la règle sous la démarche.

Le moteur résout désormais du plus spécifique au plus général —
Rules::<Démarche>::<CasDUsage>, puis Rules::<Démarche>. Le cas d'usage
« aides_financieres » d'API Entreprise porte ainsi sa propre règle de
typologie d'entité, tandis que les autres cas d'usage retombent sur la
règle de démarche (menuiserie) inchangée.
Le flag de configuration auto_instruction dupliquait une information que
le code portait déjà : une démarche sait s'auto-instruire dès qu'une
règle d'éligibilité existe pour elle. On supprime le flag et on déclenche
l'auto-instruction sur la simple présence d'une règle (convention over
configuration) — écrire la classe de règle est l'opt-in.

Conséquence voulue : HubEECertDC, qui porte une règle, s'auto-instruit
désormais aussi (cas 1 de la spec : commune → valide). Une démarche sans
règle reste en instruction humaine.
… Entreprise

La démarche Aide financière n'était qu'un véhicule de démonstration du
moteur, avant qu'une démarche réelle n'y soit branchée. Le cas d'usage
« aides_financieres » d'API Entreprise la remplace : on supprime la
démarche synthétique (modèle, définition, formulaire, règle Rules::Aide-
Financiere) pour éviter un doublon de logique de typologie et une fausse
démarche au catalogue.

Specs, seeds et scénario d'introduction rebranchés sur le formulaire
api-entreprise-aides-financieres.
…ite-api-entreprise-par-cas-usage

Cibler l'éligibilité API Entreprise par cas d'usage, sans flag
Depuis que toute démarche portant une règle s'auto-instruit à la
soumission, create_validated_authorization_request approuvait une seconde
fois une demande HubEE cert DC déjà validée automatiquement (commune →
éligible), faisant échouer db:seed:replant. On n'approuve plus
manuellement quand la soumission a déjà validé la demande.
…tion

Le verdict d’éligibilité passe d’un badge dans la carte organisation à une
mise en avant au-dessus du bloc. Quand la demande a été validée ou refusée
automatiquement, un bandeau l’annonce explicitement (vert pour la validation,
rouge pour le refus) ; sinon la mise en avant du verdict reste affichée pour
la revue humaine et disparaît quand l’éligibilité est indéterminée.
stats.rb télécharge l'export CSV public Metabase (demandes API Entreprise / API
Particulier + payload INSEE), rejoue le vrai EntityEligibility::Engine sur chaque
entité (organisation instanciée depuis le payload) et écrit stats.json :
répartition des verdicts, matrice verdict × décision humaine (pour traquer les
faux positifs/négatifs), et signaux de population (type d'entité, catégories
juridiques, codes NAF) pour concevoir de nouvelles règles.

stats.json est gitignoré : il embarque des données réelles d'organisations
(SIRET, raison sociale, payload INSEE) et reste régénérable à la demande.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Vue DSFR (index.html) consommant stats.json : filtres globaux type
d'habilitation / formulaire (en cascade) qui recalculent tout le périmètre,
cartes par verdict, matrice verdict × décision cliquable mettant en évidence les
incohérences (inéligible validé = faux positif, éligible refusé/révoqué = faux
négatif), signaux de population et table filtrable des entités.

stats.sample.json fournit un jeu synthétique (verdicts variés, croisements
incohérents) pour prévisualiser la vue sans données réelles.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Le moteur résout désormais la règle du plus spécifique (cas d'usage du
formulaire) au plus général (démarche), et lit `form.use_case`. Le stub de
formulaire ne l'exposait pas : on rejoue maintenant le moteur sur le vrai
`AuthorizationRequestForm` résolu depuis le `form_uid` (repli sur un stub
minimal si le formulaire n'existe pas en config locale). Bénéfice : les règles
ciblées par cas d'usage (ex. aides_financieres) sont correctement appliquées.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Ajoute les règles fournies par le PO : CCAS/CIAS, tarification EAJE (PSU),
tarification municipale enfance, stationnement résidentiel, aides
facultatives (département/région selon le formulaire) et tarification des
cantines (collèges/lycées selon le formulaire).

La typologie partagée (bloc communal, association, CCAS/CIAS) vit sur
Organization via les codes catégorie juridique INSEE. Les cas d'usage où
un même use_case porte des règles distinctes (aides dép./rég., cantines
collèges/lycées) sont tranchés dans la règle via le form_uid. L'engine
résout la règle la plus spécifique (cas d'usage) et ignore les namespaces
qui ne sont pas des règles.

Conformément au choix « Pas éligible = refus », ces cas d'usage
auto-refusent les organisations hors périmètre et bloquent le CTA côté
demandeur. Les scénarios de soumission API Particulier partent désormais
d'une organisation éligible au formulaire testé.
Permet aux communes de s’abonner à la démarche « Pré-dépôt de
dossier de mariage » (code DILA DDMariage) depuis le formulaire
« Démarches du bouquet de services (service-public.fr) ».

La case, son libellé dans l’introduction et sa description dans
le bloc « En quoi consistent ces démarches » sont ajoutés, et le
scope est mappé vers le processCode HubEE DDMariage à l’approbation.
Ces deux nouveaux noms d'événement permettront de distinguer une décision
d'instruction automatique (issue du moteur d'éligibilité) d'une instruction
humaine dans l'historique d'une demande.

auto_approve est lié à une Authorization (comme approve), auto_reject à une
DenialOfAuthorization (comme refuse). La contrainte SQL entity_type_validation
et la validation applicative sont mises à jour en conséquence, ainsi que les
traits de factory nécessaires au test générique parcourant NAMES.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
AutoInstructAuthorizationRequest passe désormais event_name aux organizers
d'instruction pour tracer que la décision est automatique.

CreateAuthorizationRequestEventModel privilégie context.event_name sur
state_machine_event : seul l'événement historisé change. La transition d'état et
les webhooks continuent de s'appuyer sur state_machine_event (approve / refuse),
donc les intégrateurs reçoivent les mêmes événements qu'une instruction humaine.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
auto_approve reprend le rendu d'approve (lien vers l'habilitation créée, icône
verte) et auto_reject celui de refuse (motif dépliable, icône rouge), avec des
libellés dédiés précisant que la décision est automatique.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
Précise que le moteur d'éligibilité historise auto_approve / auto_reject tout en
conservant les événements approve / refuse côté transition d'état et webhooks.

Claude-Session: https://claude.ai/code/session_01LbtsySHBUZSxJG9CQ74t9F
…nique

Le composant instructeur affichait deux blocs distincts (verdict brut et
instruction automatique) qui se recouvraient. On les unifie dans un seul
EligibilityBanner qui décide du statut à montrer (validée/refusée
automatiquement, ou éligibilité probable à confirmer manuellement) et un
composant de notice DSFR unique.
La bannière déduisait « validée/refusée automatiquement » de l’état de la
demande croisé au verdict, ce qui étiquetait aussi les décisions humaines.
On lit désormais l’historique : seuls les événements auto_approve / auto_reject
(émis par AutoInstructAuthorizationRequest) déclenchent la bannière automatique,
alignant l’affichage sur la source de vérité de l’auto-instruction.
La règle CCAS n'acceptait que les CCAS/CIAS. Les communes et les
groupements intercommunaux (communauté de communes, communauté
d'agglomération, communauté urbaine, métropole) gèrent aussi des
aides sociales via leur CCAS ou directement — ils doivent donc
être éligibles.

Techniquement : BLOC_COMMUNAL est étendu à communaute_urbaine (7343)
et metropole (7344), ce qui bénéficie également aux règles
stationnement_residentiel, tarification_eaje et tarification_municipale_enfance.
La règle Ccas s'appuie désormais sur ccas_or_cias? || bloc_communal?
au lieu de tester legal_category == :commune séparément.
Chaque ligne affiche désormais l'identifiant de la demande DataPass
avec un lien direct vers https://datapass.api.gouv.fr/demandes/ID,
pour faciliter l'investigation des incohérences repérées dans la matrice.
… de base

Permet de tester les scénarios multi-organisation avec user@yopmail.com :
Clamart reste l'organisation courante, Les Lilas est une organisation
non-courante vérifiée.
@JeSuisUnCaillou
JeSuisUnCaillou marked this pull request as draft July 6, 2026 08:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants