# Rapport — v3.23.0 (page « AO clôturés », avis complet dans les notes, sans IA)

**Se livre aux DEUX installations, à l'identique. Aucune migration, aucune dépendance composer
nouvelle, aucune suppression de fichier.**

## Le constat

La moissonneuse apporte désormais, pour chaque consultation, **le texte OCR de l'avis** du portail.
C'est important : l'avis d'un marché public marocain est un **scan**, et l'estimation du coût, le
cautionnement provisoire, le mode de passation, le délai d'exécution, les qualifications exigées et
le lieu d'exécution **n'existent que là**. La fiche du portail, elle, n'en dit pas un mot.

Deux problèmes en découlaient.

**1. Faire lire cet avis à l'IA coûterait une fortune.** L'historique se compte en milliers d'AO ;
un rattrapage à l'appel de modèle est hors de question. Or l'essentiel de ce que porte un avis se lit
avec des **repères écrits** — le socle sait déjà le faire depuis la v3.22.0 §8.

**2. La Veille devenait illisible.** Elle est une boîte de réception : ce qui compte est ce qui ferme
bientôt. Mais les AO échus y restaient, s'y accumulaient, et masquaient les échéances vivantes — alors
même que c'est APRÈS la clôture qu'un AO devient intéressant, puisque le portail y publie enfin le PV
de la commission : les concurrents, leurs montants, l'attributaire.

## §1 — L'ingestion accepte l'avis, le range dans les notes, sans IA

`POST /api/veille/ingest` et `VeilleImportService::ingererConsultation()` acceptent un champ
**`avis`**.

- **Extraction déterministe.** L'avis rejoint les sources lues par `enrichissementsDuTexte()` — les
  mêmes repères, les mêmes expressions régulières qu'en v3.22.0 §8 : la **vraie référence**, le
  **cautionnement provisoire**, l'**estimation publiée**. Il est lu en dernier : le bloc source du
  portail reste la lecture la plus régulière. Règle du socle inchangée : **on complète, on n'écrase
  pas** — seuls les champs vides (ou ceux que le socle avait lui-même posés) sont remplis.
- **Rangé dans les `notes`**, sous un séparateur reconnaissable :
  `VeilleImportService::MARQUEUR_AVIS` (`===== AVIS =====`). Ce qui précède — le bloc source, une note
  écrite à la main — est conservé **intact**, l'avis vient dessous. C'est là que vivent les
  informations **sans colonne** : mode de passation, délai d'exécution, qualifications, lieu. Leur
  donner une colonne reviendrait à parier sur un format d'avis ; on garde le texte.
- **Idempotent.** La moissonneuse repasse sur les mêmes consultations : un avis déjà rangé n'est pas
  empilé une seconde fois. Un avis n'en remplace un autre que s'il le **contient** — un OCR plus
  complet. Un texte différent est refusé : ce serait écraser.
- **Borné.** `notes` est un `TEXT` MySQL : 65 535 **octets**, et un « é » en pèse deux. Un OCR
  pathologique est coupé à 60 000 octets sur une frontière de **caractère** (`mb_strcut`). On perd
  la fin d'un avis, jamais une fiche.
- **JAMAIS d'IA sur l'`avis`.** Il n'est pas joint à `texteALire()` : aucun `analyses->queue`, aucune
  ligne `ai_analysis_runs`, zéro dirham. C'est la condition du rattrapage massif de l'historique. Le
  champ `contenu`, lui, garde exactement son comportement v3.18 : IA en file si la fonction est
  disponible.

La réponse de l'API porte un champ **`avis`** (booléen) : ce que le déversement a réellement apporté.

## §2 — Page « AO clôturés », avant la Veille

Nouvelle ressource `AoCloturesResource` — slug `ao-clotures`, `navigationSort = 5`, donc **avant** la
Veille (10), l'étoile (15) et les marchés (20).

- Elle liste les AO de **veille dont la date limite est passée** (`Tender::scopeClotures()`).
- La **Veille ne montre plus que les AO ouverts** (`Tender::scopeOuverts()` : date à venir **ou
  inconnue**). Une date inconnue vaut « ouvert » — on n'archive pas sur une ignorance.
- **C'est un pur filtre d'affichage.** Aucune donnée ne change quand une date limite passe : un AO
  clôturé reste `participation = veille`, sa fiche vit toujours dans la Veille (c'est là qu'on la
  consulte et qu'on la modifie), son URL répond, et il continue de nourrir la projection de prix.
- **Colonnes** : celles de la Veille (objet, acheteur, estimation, vraie référence, date limite),
  plus les trois que le suivi de commission remplit depuis la v3.21.0 — **attributaire**, **plis**
  (annoncés · relevés), **montant retenu**. Tri par **date limite décroissante** : le plus récemment
  clôturé en tête.
- **Filtres** : acheteur, famille (nature-aware, v3.22.1 §5), montant, période de clôture,
  **« avec concurrence » / « sans concurrence »** (`FiltresAo::concurrence()` — au moins un pli
  relevé, un attributaire connu, ou un nombre de plis annoncé), et le masquage des AO écartés.
- **Actions** : ouvrir la fiche (dans la Veille), étoiler, passer en préparation, écarter/réafficher,
  couper les alertes, liens externes, interprétation & prix IA à la demande — plus, en en-tête, les
  brouillons à valider.

Le filtre d'échéance de la Veille ne propose plus « Clôture passée » : un choix qui ne rendrait
jamais rien serait un piège. `FiltresAo::echeance()` prend un paramètre `$passee` ; les autres vues
la gardent.

## §3 — Ce qui ne bouge pas

Le retour auto en veille (v3.22) et les alertes continuent à l'identique. Les autres étapes du
parcours (⭐ AO étoile, à préparer, soumissionnés, marchés, concurrents) sont inchangées. La
projection de prix continue de lire **tous** les `Tender` : les AO clôturés, avec leur estimation et
les montants de leurs concurrents, la nourrissent comme avant.

## La recette

`tests/Feature/Veille/AoCloturesEtAvisTest.php` — 13 tests :

- un `avis` portant « … est fixée à la somme de 96.000,00 DH TTC … cautionnement provisoire …
  1.400,00 DH … » donne **estimation = 96 000**, **caution = 1 400**, et le texte de l'avis dans les
  `notes` (mode de passation, délai d'exécution, qualifications compris) ;
- **aucune ligne `ai_analysis_runs`** n'est créée par l'`avis` ; le `contenu`, lui, en crée bien une ;
- une note écrite à la main est **conservée**, l'avis vient dessous ; le même avis deux fois n'est pas
  empilé ; un OCR plus complet remplace le précédent, un texte étranger non ; un avis géant est borné
  sans casser l'UTF-8 ;
- un AO de veille dont la date limite est **hier** n'apparaît plus dans la Veille et apparaît dans
  « AO clôturés » ; un AO à **demain**, ou **sans date**, reste dans la Veille ; sa fiche reste
  consultable ; **rien n'est écrit** par la lecture ;
- « AO clôturés » affiche l'attributaire, les plis et le montant retenu, trie par clôture
  décroissante, et son filtre « avec / sans concurrence » répond dans les deux sens ;
- la page tient sur une **base vide**.

**Étanchéité** : `CrossAccessTest` teste la nouvelle page par **URL directe** — un employé et un
investisseur sont refusés sur `/ao-clotures`, et un membre du personnel sans `tenders.view` aussi.

**Non-régression** : ingestion v3.18, enrichissement v3.19, parcours v3.20, concurrence v3.21,
alertes & cycle de vie v3.22, UTF-8/acheteur/nature v3.22.1 — suite complète verte.

Les fixtures de veille des tests existants portent désormais une date limite **à venir** : la
fabrique la tirait au sort entre −6 mois et +1 mois, et une fiche née clôturée s'afficherait
maintenant dans « AO clôturés », pas dans la Veille.

## Après déploiement

**Rien à faire.** Aucune migration, aucune commande. Les AO échus se rangent d'eux-mêmes dans la
nouvelle page dès le premier affichage, et la moissonneuse peut commencer à envoyer le champ `avis`
quand elle veut : l'API l'accepte sans qu'il soit obligatoire.
