# Rapport — v3.19.0 (Veille : enrichissement léger à l'ingestion + filtres sur la page)

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

## Le constat

La moissonneuse (v3.18.0) remplit désormais la page **Veille (AO non soumissionnés)** en ratissant
**large** : tous les domaines potentiels de l'installation. Le tri fin se fait dans l'app, pas à la
source — c'était le principe. Restait que la page n'en donnait pas les moyens :

- chaque consultation arrivait **brute**. Le champ Objet portait tout le bloc du portail
  (« AOO … Services JJ/MM/AAAA REF Objet : … Acheteur public : X … JJ/MM/AAAA HH:MM »),
  l'acheteur affichait « à compléter », et la **date de clôture** n'existait nulle part ;
- il n'y avait donc **rien pour filtrer** : ni domaine fiable, ni échéance, et une recherche qui
  tombait sur du bloc brut.

Le gérant attendait l'IA « Interprétation & prix » pour lire sa propre veille — or elle est
**optionnelle**, **payante**, et **en aval**.

**Rien d'IA n'a été ajouté dans ce lot.** L'enrichissement est **déterministe** : on découpe le
texte qu'on a déjà reçu.

## §1 — Le découpage du bloc source, à l'ingestion

Nouveau : `App\Services\Tenders\VeilleBlocSource`. Il fait ce qu'un humain fait des yeux, et rien de
plus — il découpe le texte sur ses trois repères :

| Ce qu'il lit | Ce qu'il en fait |
|---|---|
| après « Objet : » jusqu'à « Acheteur public : » (ou la date finale) | l'**objet propre** de la fiche |
| après « Acheteur public : » jusqu'à la date finale | l'**acheteur** — fini le « à compléter » sur une information reçue |
| la **dernière** date `JJ/MM/AAAA [HH:MM]` du bloc | `deposit_date` + `deposit_time` |
| le bloc entier | `notes`, s'il est vide — la seule pièce qui permette de diagnostiquer un découpage de travers |

Tolérant sur la forme (« Objet: », casse variable, espaces insécables, retours à la ligne, `à 10h00`),
et **best-effort** : un repère absent laisse le champ tel quel, sans erreur. Une date impossible
(« 32/13/2026 ») est ignorée, et le candidat précédent examiné.

**La première date du bloc est la mise en ligne, la dernière est la clôture.** Les confondre
avancerait toutes les échéances : c'est la dernière qui est retenue, jamais la première.

Le découpage vit dans `VeilleImportService::ingererConsultation()` — le point de convergence
factorisé en v3.18.0. Les **deux** voies en profitent, sans une ligne de plus : l'action Filament
« Ajouter depuis un lien portail » **et** `POST /api/veille/ingest`. Le contrôleur transmet
désormais `source_meta` **entier** au service (il ne servait auparavant que de libellé de secours).

### Ce que l'enrichissement n'a pas le droit de faire

**Il complète, il n'écrase pas.** La règle de la v3.17.2 tient, et elle protège le travail d'un
**humain** — pas un pis-aller du logiciel. `estRemplacable()` en connaît désormais deux, et deux
seulement :

1. la référence provisoire `PMMP-<identifiant>` (v3.17.2) ;
2. **un objet qui n'est qu'un bloc source du portail** — reconnaissable à ses repères. Personne ne
   l'a tapé : c'est le texte reçu, déversé faute de savoir le découper. Le libellé propre le
   remplace donc, **y compris sur les fiches déjà en base** avant cette version.

Un objet saisi ou corrigé à la main ne porte pas ces repères : il reste intact, comme l'acheteur,
la date et l'heure que le gérant a fixés. Prouvé par test.

En prime : réécrire une valeur **à l'identique** n'est plus compté comme « complétée ». La
moissonneuse repasse sur les mêmes consultations ; un rapport « complétée » à chaque tour ne
voudrait plus rien dire.

## §2 — La famille d'objet, élargie et **bornée au mot**

`TenderProjectionService::FAMILLES_OBJET` couvre désormais les métiers des deux installations :

`Communication` · `Événementiel` · `Audiovisuel` · `Impression & Édition` · `Digital & Marketing` ·
`Formation` · `Incubation & Entrepreneuriat` · `Conseil & Étude` · `Autre`

Deux garde-fous, la même leçon que la moissonneuse :

- **Borne de mot.** Un mot-clé doit commencer au **début d'un mot** ; il peut se prolonger
  (« formation » mord « formations », « publicit » mord « publicitaire »), jamais l'inverse.
  « transformation » et « information » ne sont **pas** Formation, « Mohammedia » n'est **pas**
  Média. Un mot-clé préfixé de `=` exige le mot exact — sans quoi le sigle « amo » avalerait
  « amortissement ».
- **Marqueurs hors cible.** Construction, architectural, génie civil, voirie, assainissement,
  topographique, cadastral, photovoltaïque, terrassement, bâtiment, réfection… priment sur toute
  famille. « Étude architecturale d'un centre de formation » a beau contenir « étude » **et**
  « formation », c'est du bâtiment : elle tombe en **Autre**.

⚠️ **`famille()` répond toujours `null`, jamais « autre ».** C'est délibéré : la projection de prix
rapproche « les AO du même genre », et « tout ce qui n'est d'aucun genre » n'en est pas un — les
grouper fabriquerait des concurrents comparables qui ne le sont pas. Le « Autre » n'existe que pour
l'affichage et le filtre, via `familleAffichee()`. Le périmètre de l'IA « Interprétation & prix »
ne propose donc pas « Autre » : on interprète le marché d'un métier, pas celui de tout le reste.

## §3 — La page Veille : filtrer, chercher, voir ce qui ferme

- **Filtre « Famille d'objet », multi-sélection** — le filtre principal. D'un clic :
  `Communication` côté IRADA, `Formation` + `Incubation & Entrepreneuriat` côté RUWAD, ou `Autre`
  pour voir ce que la moissonneuse a ramassé hors cible. (Déduite par mots-clés, donc non stockée :
  le filtre travaille en mémoire sur `id` + `object`.)
- **Filtre « Clôture »** — `≤ 7 jours`, `≤ 15 jours`, `≤ 30 jours`, `à venir`, `passée`.
- **Colonne « Date limite »** (ex-« Date de l'AO »), triable, avec l'heure de remise des plis et le
  compte à rebours en sous-titre — **rouge** sous 7 jours, **orange** sous 15, **gris** une fois
  clôturé.
- **Tri par défaut : les clôtures les plus proches d'abord.** Les échéances à venir en tête, de la
  plus proche à la plus lointaine ; les AO déjà clos ensuite, du plus récent au plus ancien ; ceux
  sans date en dernier. La veille est devenue une boîte de réception — ce qui ferme bientôt compte
  plus que ce qui a fermé hier.
- **Recherche** sur objet + acheteur + référence (déjà en place ; elle porte enfin sur un objet
  **propre**).

## Pourquoi aucune migration

La date de clôture est écrite dans **`deposit_date`**, et l'heure dans **`deposit_time`** —
c'est-à-dire dans les colonnes qui ont **déjà exactement ce sens** : le formulaire de veille les
présente depuis l'origine comme « date limite de dépôt relevée sur l'avis », et
`TenderProjectionService` les passe au modèle sous le nom `date_limite_depot`.

Ajouter un `date_limite` parallèle aurait créé **deux dates limites divergentes** sur une même
fiche — et surtout, « Reprendre en soumission » ne fait que basculer `participation` sur le **même
enregistrement** : l'AO repris aurait perdu sa date de clôture en entrant dans le pipeline. Le
prompt autorise explicitement la réutilisation d'une colonne qui a déjà ce sens ; c'est le cas.

`deposit_time` rejoint au passage les champs importables (écran de correspondance CSV, brouillons
d'extraction) et le formulaire de veille, sous le libellé « Heure limite ». Le libellé du champ
`deposit_date` dans l'écran de correspondance CSV est resté « Date de l'AO » — c'est sur lui que se
fait le rapprochement automatique des en-têtes d'un fichier existant.

## La recette

`tests/Feature/Veille/VeilleEnrichissementTest.php` — 30 tests, aucune donnée réelle :

- **Parsing** : objet propre + acheteur + date + heure extraits d'un bloc réaliste ; la **dernière**
  date l'emporte sur la mise en ligne ; morceaux manquants → pas d'erreur ; date impossible ignorée ;
- **Ingestion** : la fiche est remplie **sans qu'aucun job ne parte en file** (`Queue::assertNothingPushed`),
  ne se dédouble pas, et ne se déclare pas « complétée » au second passage ;
- **On n'écrase pas** : objet, acheteur, date et heure corrigés à la main survivent à une nouvelle
  ingestion ; un bloc brut déjà déversé en objet, lui, est bien remplacé ;
- **Famille** : 14 cas, dont « transformation », « information », « Mohammedia » et
  « étude architecturale d'un centre de formation » → **Autre** ;
- **Filtres** : famille (un choix, plusieurs choix, « Autre »), recherche sur objet/acheteur/
  référence, clôture (7/15/30/à venir/passée), tri ;
- **Base vide (RUWAD)** : page + tous les filtres se rendent sans un seul AO.

Non-régression vérifiée par la suite existante : `VeilleIngestApiTest`, `VeilleImportTest`,
`VeilleIaTest`, `VeilleScopingTest`, projection v3.16. Participation = **veille** ⇒ **aucun impact
KPI**, comme depuis la v3.17.0.

## À vérifier après déploiement

1. Ouvrir **Veille (AO non soumissionnés)** : les fiches ramassées par la moissonneuse doivent
   afficher un **objet lisible**, un **acheteur** et une **date limite** — les anciennes comprises,
   dès le prochain passage de la moissonneuse sur la même consultation.
2. Filtrer sur la ou les familles de l'installation, puis sur « Clôture ≤ 15 jours ».
3. Vérifier qu'un objet corrigé à la main **reste** corrigé après un nouveau passage.
