Moteur d'éligibilité des entités (hackathon) - #1658
Draft
skelz0r wants to merge 53 commits into
Draft
Conversation
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
force-pushed
the
hackathon/entity-validation
branch
from
June 30, 2026 13:07
2b6b4a6 to
5a252a7
Compare
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)
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é
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
marked this pull request as draft
July 6, 2026 08:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
likely_eligible).Contenu (par PR mergée sur la branche)
EntityEligibility::Engine/Verdict(5 statuts) /Rules::Base.Rules::HubEECertDC(commune) etRules::APIEntreprise(menuiserie), DSL de verdict.Organization(activite_principale,categorie_juridique…) ; plus aucune règle ne replonge dansinsee_payload.Organization#entity_type(niveau 1 de la catégorie juridique) etRules::AideFinanciere, première règle à exercerlikely_eligible.AideFinancierenavigable, badge DSFR « Valide / Invalide » + explication de la règle dans la carte organisation (API-7004), seeds des 3 cas de démonstration.Périmètre / suites
Doc :
docs/technique/moteur_eligibilite.md.Demandes à tester (instruction)
Vue instructeur :
/instruction/demandes/:id(connecté endatapass@yopmail.com).L’auto-instruction est déclenchée à la soumission (
SubmitAuthorizationRequest→AutoInstructAuthorizationRequest) : la bannièrevalidée / refusée automatiquements’appuie sur les événementsauto_approve/auto_rejecthistorisé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).eligiblelikely_eligibleineligibleeligibleLes demandes API Entreprise sans cas d’usage renvoient un verdict
unknown: aucune bannière n’est affichée.