Aller au contenu

Lot 1, séquence B3 — Écrans des référentiels

1er octobre 2026. Mise en œuvre de la séquence B3 du plan du Lot 1, écran par écran. Les huit écrans sont livrés. ADR : 0041 (MapLibre), 0042 (react-jsonschema-form).

Écran État Contrat d’API
Campagnes Livré (lecture) 1.2.0
Produits et attributs Livré (lecture, aperçu du formulaire) 1.3.0
Numérotation Livré (aperçu en lecture) 1.4.0
Producteurs, groupements Livré (liste, fiche, fusion) 1.5.0
Parcelles Livré (liste, fiche, carte, import GeoJSON) 1.6.0
Certificats et statuts Livré (lecture, synthèse, verdict) 1.7.0
Contrats Livré (lecture : lignes, primes, réfaction lisible) 1.8.0
Capacités Livré (lecture, double validation) 1.9.0

Trois écrans seulement écrivent : Producteurs (fusion, review.resolve), Parcelles (import de polygone, plot.survey) et Capacités (validation, capacity.approve). Ce sont les trois seuls cas où le catalogue des permissions (EC-038) a déjà le droit qu’il faut. Pour tout le reste (créer une campagne, éditer un produit, saisir un certificat ou réviser un statut, créer ou activer un contrat, proposer une capacité ou fixer un rendement), les services existent sans route, parce qu’aucune permission ne les couvre et qu’on n’en invente pas : EC-082, EC-083, EC-085, EC-086, EC-087. Il faut décider ces droits avant d’ouvrir les routes.

GET /v1/campagnes rend désormais, pour chaque campagne, le pack figé à son ouverture : pack: { code, libelle, version } (contrat 1.2.0, ajout additif). L’écran liste code, nom, année, statut, période (réelle, sinon prévue), pack et version du modèle de numérotation.

Lecture seule : la création et l’ouverture n’ont ni service ni route (EC-082).

GET /v1/produits (contrat 1.3.0) rend les produits de l’organisation avec leurs attributs typés, dans l’ordre du pack : code, noms fr et en, unité, matière première liée, pack d’origine, et pour chaque attribut son JSON Schema, son caractère obligatoire et son ordre. Accessible à tout membre actif.

L’écran liste les produits, montre la fiche et les attributs, puis un aperçu du formulaire généré par react-jsonschema-form depuis ces schémas (ADR 0042). Rien n’est enregistré : les valeurs d’attributs se saisissent sur les lots (Lot 2). Le formulaire et le serveur valident le même JSON Schema avec Ajv (test B3-01 : même verdict que validerAttributs). Le thème « Registre » (libellé au-dessus, erreur en clair) est dans apps/backoffice/src/formulaire/. L’écran est chargé à la demande.

Lecture seule : la création et la modification d’un produit n’ont ni service ni route (EC-083).

GET /v1/numerotation/apercu?campagne=&produit=&nature= (contrat 1.4.0) rend le numéro que la prochaine attribution donnerait, son rang et le modèle figé de la campagne (segments). Il lit derniere_valeur + 1 de la séquence : rien n’avance, rien n’est verrouillé, donc l’aperçu est indicatif (test : deux appels donnent le même numéro, aucune ligne de séquence créée). Le format n’est calculé qu’à un endroit, numeroOfficiel du domaine ; attribuerNumero et l’aperçu partagent le même chargement (chargerContexte) et le même formatage.

Erreurs : campagne_introuvable, produit_introuvable (404), campagne_non_ouverte, sequence_hors_capacite, segment_manquant (409), parametre_invalide (400). Les messages fr et en sont dans les catalogues.

L’écran suit la campagne active du sélecteur : si elle n’est pas ouverte, il le dit. Les codes de nature (MP, REC, NET, PF) restent à confirmer (EC-068). Le design system §7 n’a pas d’entrée « Numérotation » : elle est placée sous Référentiels en attendant (EC-084).

Quatre routes (contrat 1.5.0) :

Route Droit Rend
GET /v1/groupements membre actif Groupements avec leur nombre de producteurs actifs
GET /v1/producteurs membre actif Liste filtrable (groupement, statut, q, limite ≤ 500, 200 par défaut), avec en_controle
GET /v1/producteurs/{id} membre actif Fiche, doublons de pièce, contrôles ouverts
POST /v1/producteurs/{id}/fusionner review.resolve La fiche retenue, après fusion
  • Doublon signalé, jamais refusé (AV1-02) : la fiche montre les autres fiches actives qui portent la même pièce (comparaison normalisée) et les contrôles ouverts.
  • Numéro de pièce masqué : seuls les trois derniers caractères sortent de l’API (numero_piece_masque). Le numéro complet ne quitte pas le serveur ; aucun test ne le retrouve dans la réponse.
  • Fusion : le corps est { retenu_id, motif } et l’absorbée est dans le chemin. L’API appelle la fonction de la base (app.fusionner_producteurs), qui désactive la fiche absorbée, la renvoie vers la retenue et lève ses contrôles de doublon avec la décision fusion (le même acte, pas deux). Rien n’est supprimé. Refus : motif vide ou même fiche (fusion_refusee, 400), fiche absente ou déjà fusionnée (fiche_introuvable, 404), auteur de la commande (levee_par_auteur, 403, SB-12).
  • Droit de la fusion : review.resolve, comme la levée d’un contrôle, puisque la fusion en lève. À revoir avec EC-071.
  • Les lectures n’exigent que d’être membre actif : le catalogue des permissions n’a pas de droit de lecture des référentiels (EC-038). Les données personnelles (téléphone, village) sont donc lisibles de tout membre ; à revoir avec EC-071.

L’éligibilité au groupe (RG-024) n’est pas affichée : elle attend la route des surfaces mesurées (écran Parcelles).

Trois routes (contrat 1.6.0) :

Route Droit Rend
GET /v1/parcelles membre actif Liste filtrable (producteur, groupement, q, limite ≤ 500), avec la version en vigueur et en_controle
GET /v1/parcelles/{id} membre actif Fiche : versions de polygone, écart de surface, contrôles ouverts
POST /v1/parcelles/{id}/polygone plot.survey La nouvelle version (201)
  • Écart de surface (GPS-04) : surface déclarée, surface mesurée par la base et incertitude (périmètre × précision médiane). anomalie est vrai seulement si l’écart dépasse l’incertitude ; dans la marge, l’écran le dit en toutes lettres. Sans polygone valide, ecart est nul. Le calcul est celui du domaine (evaluerEcartDeLaParcelle).
  • Import : GeoJSON Polygon, Feature ou FeatureCollection à un seul polygone, sans trou (EC-048). Chaque import est une nouvelle version (ajout seul). L’API ne ferme ni ne corrige rien : un anneau non fermé est refusé (polygone_illisible), un polygone invalide est conservé et signalé par un contrôle polygone_invalide, jamais réparé. Un chevauchement ouvre un contrôle chevauchement_parcelle (tolérance paramétrée, A5).
  • Droit : plot.survey, le droit du relevé sur le terrain (parcelle.relever).
  • Carte (ADR 0041) : MapLibre, chargé à la demande (environ 1 Mo, un fichier séparé), fond uni (EC-081). Version en vigueur en trait plein, anciennes en pointillé, polygone invalide en couleur terre. Les contours sont dessinés en lignes : le découpage en tuiles de MapLibre écarte un polygone auto-intersecté, que le contour en ligne montre tel quel (constaté sur un nœud papillon, d’où ce choix). Exemple dans /_galerie en développement.
  • Les tests de carte portent sur la préparation des données (B3-02, B3-03), pas sur le rendu WebGL.

Le client de l’API est maintenant créé à la première utilisation : la galerie s’ouvre sans configuration Keycloak.

Trois routes de lecture (contrat 1.7.0), ouvertes à tout membre actif :

Route Rend
GET /v1/certificats?referentiel=&date= Certificats par date de fin croissante, avec etat et alerte calculés à la date
GET /v1/conformite/statuts?date= Par référentiel actif, le nombre d’unités par état de conformité à la date
GET /v1/conformite/verdict?referentiel=&producteur=|parcelle=&date= Le verdict de revendication d’une unité
  • « Expiré » se calcule, il ne se stocke pas : etat vient de etatCertificat du domaine et alerte de alerteCertificat, avec le seuil de 60 jours (RG-016, constante PARAMETRES_CERTIFICATION). Test : le même certificat est « en vigueur » au 01/10/2026 et « expiré » au 01/12/2026, sans que la base change. La date vaut aujourd’hui (UTC, horloge de l’API) par défaut ; une date inexistante est refusée (parametre_invalide).
  • Synthèse : pour chaque (unité, référentiel), la dernière ligne d’historique dont la date d’effet n’est pas postérieure à la date, la même règle que app.statut_conformite_a. Une unité sans statut n’est pas comptée.
  • Verdict : verdictRevendication (A6) puis peutRevendiquer du domaine, un seul endroit qui décide. La réponse porte la qualité revendiquée (tampon de qualité), ou la raison du refus et la qualité sous laquelle l’unité reste vendable.
  • Aucune information par la couleur seule : chaque état de certificat porte un symbole et un libellé ; le vert ne sert qu’au tampon « bio ».

Lecture seule : la saisie d’un certificat, d’un fournisseur avec son certificat et la révision d’un statut de conformité (motif obligatoire, RG-150) existent en services mais n’ont pas de route : aucune permission du catalogue (EC-038) ne les couvre (EC-085).

Deux routes de lecture (contrat 1.8.0), ouvertes à tout membre actif : GET /v1/contrats (filtres campagne, statut, type) et GET /v1/contrats/{id}.

  • Valeur engagée : somme des lignes au prix de base, hors primes, par devise, avec un arrondi par ligne (valeurLigne du domaine). Test d’intégration : 10 000 kg à 150 XOF/kg valent 1 500 000 XOF ; 2 000 kg à 2,50 EUR/kg valent 500 000 centimes d’euro ; les deux totaux ne se mélangent pas.
  • Formule de réfaction lisible : l’API rend la formule validée par son schéma (FormuleRefaction), l’écran la met en phrase avec les pourcentages et le mode d’arrondi. Une formule stockée qui ne passe pas le schéma est rendue nulle et signalée « formule illisible » : elle n’est jamais devinée.
  • Primes : en montant (dans la devise de la ligne) ou en taux du prix de base. Aucun montant sans devise.
  • Campagne : l’écran suit la campagne active du sélecteur, avec une case pour voir toutes les campagnes.

Lecture seule : la création d’un contrat en brouillon et son activation existent en services (enregistrerContrat, activerContrat) sans route, faute de permission dans le catalogue (EC-086).

Deux routes (contrat 1.9.0) :

Route Droit Rend
GET /v1/capacites?campagne=&etat= membre actif La version courante de chaque (cible, campagne), avec circuit de validation et solde
POST /v1/capacites/{id}/validations capacity.approve La capacité après la décision
  • La base calcule, l’écran affiche. capacite_g (surface mesurée × rendement) est la colonne générée de la base ; rien de ce qui compte n’est saisi. L’état (en_attente, validee, refusee) est celui de etat_validation_capacite, le seul endroit qui décide. Le motif du rendement (RG-023) est celui de la révision connue à la création de la capacité.
  • Double validation : la validation se fait sous un rôle (direction ou rsci) que le validateur prouve par l’ensemble de permissions qu’il détient, en plus de capacity.approve (EC-075). La séparation des tâches est tenue par le service et par la base (SB-11) : l’auteur ne valide pas sa capacité (separation_des_taches), une personne ne valide pas sous deux rôles, un rôle ne valide qu’une fois (validation_refusee, 409). Un refus exige un commentaire (commentaire_requis, 400). Une décision refusée du service annule la transaction : aucune validation à moitié écrite.
  • Applicable (EC-012) : applicable est vrai seulement si cette version est la capacité applicable de la cible. Test : après un nouveau relevé du polygone, la capacité reste validee mais n’est plus applicable, et l’écran l’écrit.
  • Solde : consommé, reste (négatif en cas de dépassement, jamais masqué, RG-022), taux et niveau (normal, seuil, depassement). Le niveau vient de evaluerCollecte du domaine sur une collecte nulle, avec le seuil de 80 % (constante PARAMETRES_CAPACITE). Il s’affiche avec la jauge du design system. Absent tant que la capacité n’est pas validée.
  • L’écran suit la campagne active du sélecteur.

Hors écran : la proposition d’une capacité et la saisie d’un rendement (motif obligatoire) existent en services (proposerCapacite, definirRendement) sans route, faute de permission dans le catalogue (EC-087).