---
suivi: 1661
date: 2026-09-22
sujet: Module concurrence de marché (dossier + vs marché grille)
chantier: Foncier / Faisabilité
type: feature
statut: poussé
hash: cae64330
fichiers:
  - database/migrations/2026_09_22_240000_create_dossier_foncier_concurrence_tables.php
  - app/Support/Foncier/ConcurrenceTranches.php
  - app/Support/Foncier/FoncierReferentiels.php
  - app/Support/Foncier/DossierFoncierTabs.php
  - app/Models/DossierFoncier.php
  - app/Models/DossierFoncierConcurrent.php
  - app/Models/DossierFoncierConcurrentLot.php
  - app/Services/Foncier/DossierFoncierConcurrenceService.php
  - app/Http/Controllers/Foncier/DossierFoncierConcurrenceController.php
  - app/Http/Controllers/Foncier/DossierFoncierController.php
  - app/Http/Controllers/Foncier/ScenarioFaisabiliteController.php
  - routes/web.php
  - resources/js/Composables/useTabAnchor.js
  - resources/js/Composables/useTableurCellules.js
  - resources/js/Components/Foncier/DossierConcurrenceTab.vue
  - resources/js/Pages/Foncier/Dossiers/Show.vue
  - resources/js/Pages/Foncier/Scenarios/Show.vue
  - resources/js/utils/seuilsIndicateursEtudeFi.js
  - tests/Unit/DossierConcurrence1661SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1661_module_concurrence.md
---

# SUIVI #1661 — concurrence de marché

## PROMPT ENVOYÉ

Module concurrence au niveau DOSSIER. Saisie inline tableur.
Prix de référence par tranches fixes, lots vendus + viabilisés,
pondéré par le coefficient. Colonne vs marché + écart global
dans la grille. Aucun calcul de faisabilité modifié. AppShell
non touché.

## FICHES LUES

- **#1642 LEÇON** — grammaire tableur `registerCell` / CELL_* ;
  bande KPI. Colonne vs marché ajoutée SANS changer hauteurs
  ni `title="Calculé (moteur)"` (sinon le compteur #1642 casse).
- **#1654 LEÇON** — un seul objet de barème + `palierSeuilIndicateur`.
  Écart : `SEUILS_POSITIONNEMENT_MARCHE` dans le même fichier,
  `zero_neutre: false` (0 % = aligné).
- **#1203** — KEYS PHP + JS doivent rester alignées ; AppShell
  label fallback `key.toUpperCase()` → CONCURRENCE sans toucher
  AppShell.vue.
- **#1660** — Show.vue en série ; rebase origin/main avant Vite.

Anti-collision : dernier numéro = **#1660**. **#1661 est libre.**

## SYNTHÈSE

### Tables

`dossier_foncier_concurrents` + `dossier_foncier_concurrent_lots`.
varchar/decimal, SoftDeletes, FK courtes, pas de colonne STORED.
Coefficient DECIMAL(5,2) = pourcentage (100.00 = 100 %).

### Prix de référence

Σ (prix_m² × coeff) / Σ coeff, lots vendus ET viabilisés.
Moins de 3 réf. : gris + « peu de références ». Tranche vide : « — ».

Exemple : 600 €/m² à 100 % et 700 €/m² à 60 %
→ (60 000 + 42 000) / 160 = **637,50 €/m²**.

### vs marché

Colonne et bande UNIQUEMENT si `a_des_concurrents`. Calcul front
hors moteur (surface_cessible et prix_m2 déjà calculés).

Paliers proposés (ratio, `ecart_vs_marche`) :
< −5 % vert_jaune (prudent) · −5 à +5 % vert (aligné) ·
+5 à +12 % orange (optimiste) · > +12 % rouge (alerte).

### Closures

Aucune closure `use ()` ajoutée dans le service (boucles foreach).
Controller : aucune. withCount existant inchangé.

## DÉPLOIEMENT-TEST

```
git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan route:clear
php artisan migrate --force
php artisan journal:sync
```

Recette V1 DF-2026-0012 : consolidé 100 583,31 € inchangé.
Compte tiers sans GS_ADMIN_ERP : `foncier.lecture` + habilitation
lecture, **sans** `foncier.prix` ; autre groupe M365 sans
habilitation dossier pour le 403.

## LEÇON

AppShell labels : fallback `toUpperCase()` permet d’ajouter un
onglet sans toucher AppShell si le libellé est le code en
majuscules. SoftDeletes + `DB::table` : `whereNull('deleted_at')`
sur lots ET concurrents, pas seulement Eloquent. 0 % d’écart n’est
pas un « scénario vide » : `zero_neutre: false` sur le barème,
sans changer l’algo des indicateurs d’étude Fi.
