# Rapport — v3.17.0 (les liens toujours à portée, et la base « AO non soumissionnés »)

**Version du socle : se livre aux DEUX installations, à l'identique, par le même tarball.
Deux migrations additives et idempotentes. Aucune dépendance nouvelle, aucune suppression.
Deux commits : §1 les liens, §2 la veille.**

## 1. Les liens Drive et Portail, francs et toujours présents

Les liens n'étaient qu'à demi là : une petite icône sur l'objet de l'AO (v3.16), le `drive_link`
enfoui dans les formulaires. Partout où on consulte un AO ou un marché, deux boutons francs
ouvrent désormais dans un onglet neuf.

**Le bouton est TOUJOURS présent, dans l'un de deux états visuels distincts** — c'est la demande
précise du gérant, et c'est ce qui donne toute sa valeur au lot :

| État | Aspect | Au clic |
|---|---|---|
| Lien présent | couleur pleine | ouvre le lien (onglet neuf, `rel="noopener"`) |
| Lien absent | couleur atténuée | ouvre une **saisie rapide** pour coller le lien sur-le-champ |
| Absent, sans droit `update` | atténué, **inerte** | rien — jamais un bouton mort trompeur |

On repère ainsi, sur une liste entière, où un lien manque — et on le complète là où on le voit.

Une seule fabrique, `App\Filament\Support\LiensExternes`, sert les **quatre** écrans : tableau des
AO, tableau des marchés, fiche d'un AO, fiche d'un marché. Elle expose `driveAction()`,
`portailAction()` et une colonne groupée « Liens ». Écrire ces boutons quatre fois aurait garanti
qu'ils divergent.

**Un marché tire son lien portail de son AO d'origine** (`tender?->pmmpLink()`) ; un marché sans AO
n'a pas de consultation à montrer, et son bouton le dit plutôt que de proposer une saisie qui
n'aurait nulle part où aller.

`App\Support\PortailUrl` fait les **deux sens** : construire l'adresse du suivi de commission
depuis `refConsultation` + `orgAcronyme` (`Tender::pmmpLink()` l'appelle désormais — la
construction ne vit plus qu'à un endroit), et **lire ces deux identifiants d'une URL collée** :
ordre des paramètres quelconque, casse indifférente, adresse partielle, jamais d'exception. Une
adresse illisible est conservée telle quelle — mieux vaut un lien qu'on ne sait pas décomposer que
pas de lien.

## 2. La base « AO non soumissionnés » (veille concurrentielle)

### 2.1 L'ingénierie : une dimension, pas une table

Une fiche de veille est un `Tender` avec `participation = veille`. Ce choix commande tout le
reste : elle réutilise **sans les dupliquer** les résultats par concurrent, les fiches
concurrents, `pmmpLink()`, les liens externes — et surtout elle **enrichit automatiquement
l'estimation de prix**, puisque `TenderProjectionService` puise ses candidats dans *tous* les
`Tender`.

**La règle d'or, tenue par `Tender::scopeSoumissions()`** :

- **Nos indicateurs de performance ne comptent que les soumissions.** Taux de réussite, CA gagné,
  cautions bloquées, écart moyen, prochain dépôt, comptes du bandeau, liste « Appels d'offres »,
  kanban du pipeline, rappels de dépôt, digest quotidien, outils de l'assistant, PDF d'analyse
  globale, pickers de documents de soumission. Une veille n'y entre **jamais**.
- **L'intelligence de marché lit tout.** `CompetitorInsightService` et `TenderProjectionService`
  n'appliquent pas le scope : un prix relevé chez un concurrent est un fait, d'où qu'il vienne.

Conséquence directe, vérifiée par un test : la **« Projection du dépôt (IA) » de la v3.16** voit
désormais tout le marché observé de l'acheteur, pas seulement nos propres AO — **meilleure
estimation, sans code spécifique**.

### 2.2 La page

Ressource Filament dédiée (`/veille`), dans « Marchés & appels d'offres ». Table : référence,
acheteur, objet, date de l'AO, estimation, **résultat** (vainqueur + prix gagnant), **concurrence**
(plis annoncés / relevés), et la colonne « Liens » du §1. Filtres : acheteur, **famille d'objet**
(via `TenderProjectionService::famille()`), période, « résultat connu ? ».

Formulaire allégé : **ni bordereau, ni checklist, ni caution, ni « notre offre », ni notre étape
d'élimination** — y remettre ces champs inviterait à les remplir, et un chiffre saisi là fausserait
une lecture de marché. L'onglet « Concurrence » est le relation-manager existant, réutilisé tel quel.

⚠️ **Autorisations** : le modèle étant partagé avec `TenderResource`, Filament résout la même
`TenderPolicy` — une classe de policy distincte n'est pas possible sur un modèle unique. C'est
cohérent : mêmes données, même domaine (`tenders.*`).

### 2.3 Les quatre alimentations

Toutes convergent vers `VeilleImportService`, qui tient deux promesses sans exception :

- **dédoublonnage** — clé `ref_consultation` si connue, sinon `reference` + `buyer_name` ;
- **on complète, on n'écrase pas** — sur une fiche existante, seuls les champs **vides** sont
  remplis. Un CSV approximatif ou une extraction IA ne doit pas effacer ce qu'un humain a corrigé.

1. **Saisie manuelle** — le formulaire allégé.
2. **Lien portail collé** — `PortailUrl` extrait les deux identifiants ; `VeillePortailService`
   tente ensuite, **best-effort** (5 s, `try/catch`), de lire la page publique pour pré-remplir
   l'objet. Le portail sert l'essentiel derrière une session : l'échec est **attendu**, jamais une
   erreur — la fiche est créée avec ses identifiants et se complète à la main ou par la méthode 4.
3. **Import CSV / collage** — `fgetcsv` sur un flux mémoire, séparateur `,` ou `;` deviné, écran de
   **correspondance des colonnes** (rapprochement automatique quand les en-têtes portent le bon
   nom), upsert dédoublonné, **rapport de fin** (créées / complétées / doublons / lignes en erreur).
   Montants « 1 250 000,50 DH » et dates `15/03/2026` compris. Fichier vide et colonnes manquantes
   tolérés.
4. **Extraction IA d'un avis** — `AiFeature::VeilleExtraction`, par la file. **Le résultat n'est
   jamais écrit seul** : il est rangé comme brouillon dans `ai_analysis_runs.options`, et l'action
   « Brouillons à valider (N) » le présente pré-rempli à l'exploitant. Un document mal lu ne doit
   pas polluer en silence la base qui sert ensuite à estimer nos prix.

**Bonus** : « Basculer en veille » (finalement non soumissionné) et « Reprendre en soumission ».

### 2.4 L'estimation de prix

- **Automatique** — décrit en §2.1 : la veille nourrit la projection du dépôt sans code en plus.
- **Dédiée** — « Interprétation & estimation de prix (IA) » sur un acheteur ou une famille d'objet.
  `MarketInterpretationService` assemble le corpus **déterministe** (AO observés, résultats connus,
  prix gagnants, **écart habituel du gagnant à l'estimation**, acteurs et leurs écarts, mode
  d'attribution déduit des résultats en base), puis le narratif IA passe par la file.

  **Mêmes gardes que la projection, et écrites une seule fois** : le recalage est délégué à
  `TenderProjectionService::recaler()` — tout nom absent de la base est écarté, les prix affichés
  sont ceux du socle, et `target ≥ floor` **toujours**. Écrire un second recalage aurait garanti que
  les deux gardes divergent.

## 3. Recette

Suite complète verte, `pint`, `npm run build`. Quatre fichiers de tests ajoutés :

- `tests/Unit/PortailUrlTest.php` — les deux sens de l'URL du portail, tolérance et refus nets ;
- `tests/Feature/Tenders/LiensExternesTest.php` — les deux états, la saisie rapide (Drive et
  Portail, depuis un AO comme depuis un marché), l'inertie sans droit `update`, le marché sans AO ;
- `tests/Feature/Veille/VeilleScopingTest.php` — **la règle d'or** : une veille ne bouge ni le taux,
  ni le CA, ni les comptes, n'apparaît pas dans « Appels d'offres », apparaît sur « Veille », et
  **ressort dans les candidats d'une projection** ; les deux bascules ;
- `tests/Feature/Veille/VeilleImportTest.php` — dédoublonnage (portail et référence+acheteur),
  complétion sans écrasement, concurrents sans doublon, CSV (dont vide, colonnes inconnues, formats
  français), lien portail injoignable, base vide ;
- `tests/Feature/Veille/VeilleIaTest.php` — extraction par la file sans appel synchrone, **brouillon
  qui ne crée aucune fiche**, validation qui la crée et vide la file, échec gracieux sans fonction
  active ; corpus observé et mode d'attribution, **recalage** (nom inventé écarté, prix relevé au
  plancher), périmètre vide sans appel au modèle.

Deux points d'appui trouvés en chemin :

- `tenders.reference`, `object` et `buyer_name` sont **NOT NULL** depuis l'origine du socle. Une
  fiche de veille naît souvent sans acheteur ni objet (un lien du portail n'en donne aucun) : on
  pose une chaîne **vide** plutôt qu'un texte inventé, la référence retombe sur `PMMP-<ref>`, et
  l'interface affiche « à compléter ». Une ligne sans référence **ni** identifiant est refusée et
  rapportée — une fiche sans nom serait introuvable et impossible à dédoublonner.
- `ai_analysis_runs` n'avait pas de place pour les **paramètres d'entrée** d'une analyse sans
  document (le périmètre d'une interprétation) ni pour une **sortie à valider** (un brouillon).
  D'où la colonne `options` ; `result` garde son rôle de résumé des pièces.

Non-régression vérifiée : les 3 PDF (v3.15), la projection du dépôt et la refonte de page (v3.16),
l'aide à la décision GO/NO-GO, la note d'analyse IA, et les policies existantes.

## 4. Livraison

```bash
git diff --name-only --diff-filter=ACMR v3.16.0 v3.17.0
```

**Les deux installations reçoivent le même paquet.** Les migrations sont additives et idempotentes :
`php artisan migrate --force` est sûr des deux côtés. La reprise de `participation` est explicite —
tout AO déjà en base est une vraie soumission, il n'a jamais existé de fiche de veille avant cette
version.

1. **Sauvegarde phpMyAdmin de la base** (avant tout) ;
2. extraction du paquet ;
3. `php artisan migrate --force` ;
4. `php artisan optimize:clear` (une ressource et des routes apparaissent).

Les deux fonctions IA nouvelles (« Extraction d'un avis pour la veille », « Interprétation du
marché & prix conseillé ») sont actives par défaut et se désactivent dans les Réglages IA ; sans
clé API, leurs actions ne s'affichent simplement pas.

## 5. Fichiers

**Ajoutés** — `app/Support/PortailUrl.php`, `app/Filament/Support/LiensExternes.php`,
`lang/fr/liens.php`, `app/Enums/TenderParticipation.php`,
`app/Services/Tenders/VeilleImportService.php`, `app/Services/Tenders/VeillePortailService.php`,
`app/Services/Tenders/MarketInterpretationService.php`,
`app/Services/AI/VeilleExtractionService.php`, `app/Services/AI/MarketInterpretationAiService.php`,
`app/Filament/Admin/Resources/Veille/**` (ressource, pages, schémas, table, actions),
`database/migrations/2026_08_28_100000_add_participation_to_tenders_table.php`,
`database/migrations/2026_08_28_100100_add_options_to_ai_analysis_runs_table.php`,
et les cinq fichiers de tests ci-dessus.

**Modifiés** — `app/Models/Tender.php`, `app/Models/AiAnalysisRun.php`, `app/Enums/AiFeature.php`,
`app/Enums/AiAnalysisKind.php`, `app/Services/AI/AiManager.php`,
`app/Services/AI/AiAnalysisDispatcher.php`, `app/Jobs/RunAiAnalysis.php`,
`app/Services/Tenders/TenderPipelineService.php`, `app/Services/Tenders/PipelineAoService.php`,
`app/Services/Notifications/DailyDigestService.php`, `app/Services/AI/Assistant/AssistantTools.php`,
`app/Console/Commands/SendTenderDepositReminders.php`,
`app/Http/Controllers/TendersOverviewPdfController.php`,
`app/Filament/Admin/Resources/Tenders/**` (ressource, table, fiche, pipeline),
`app/Filament/Admin/Resources/Projects/**` (table, fiche),
`app/Filament/Admin/Resources/CompanyDocuments/Pages/ManageCompanyDocuments.php`,
`app/Filament/Admin/Resources/DocumentTemplates/Tables/DocumentTemplatesTable.php`,
`tests/Feature/Documents/LecteurDeDocumentsTest.php`, `CHANGELOG.md`.

**Aucune suppression, aucun renommage.**
