---
suivi: 1288
date: 2026-08-12
sujet: cloisonnement_foncier
chantier: module-foncier
type: feature
statut: poussé
hash: 6567b8fa
fichiers:
  - tools/diag/verif_cloisonnement_foncier_1288.php
  - database/migrations/2026_08_12_140000_record_habilitations_historique_1288.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1288_cloisonnement_foncier.md
---

## PROMPT ENVOYÉ

CLOISONNEMENT DU MODULE FONCIER. Migration + ACL + refus serveur.
Suite au diagnostic #1285 (prod 12/08). Module foncier **pas en production**
(4 parcelles test, 2 secteurs, 1 dossier) → seul moment sûr pour casser la
visibilité ouverte. Règles R1–R7 tranchées par Robin. Serialisation après
#1286 / #1287.

## SYNTHÈSE

### Contexte — pourquoi maintenant

Le module n’est **pas en production**. Le diag #1285 annonce ~34/36 users à
0 parcelle en mode bloquant : **sans conséquence aujourd’hui**, bloquant dans
trois mois. Ne pas préserver la visibilité actuelle : la casser est attendu.

### Règles R1–R7

| Règle | Contenu |
|-------|---------|
| **R1** | Parcelle visible si **au moins une** branche : **a** secteur (`codesInseePourUtilisateur`) · **b** créateur · **c** habilité sur un dossier qui porte la parcelle (prédomine) · **d** *non-visibilité* — voir R5 · **e** paquet négociation (propriétaire déjà touché dans le périmètre, **sans rebond** co-indivisaire) |
| **R2** | Dossier : créateur ∪ habilitation ∪ bypass **Admin + DG + DSI**. Liste blanche : sans ligne = invisible hors créateur/bypass |
| **R3** | `GS_ADMIN_ERP` + `GS_DIRECTION_GENERALE` voient tout (parcelle + dossier) ; mention « vue complète » (#1283) |
| **R4** | Attribuer hab. dossier : **créateur + Admin + DG** — **pas DSI** |
| **R5** | Hab. **parcelle** = autorisation d’**usage** (rattacher / modifier), **pas** de visibilité. Visibilité = R1 uniquement. R1d ≠ branche de vue |
| **R6** | Niveaux dossier : `lecture` \| `gestion` (déjà à l’écran ACCES) |
| **R7** | Historique : retrait = soft (`retire_par_id` / `retire_le`), pas DELETE dur au sync |

**Bypass vue parcelle** : Admin + DG **uniquement** (DSI hors bypass parcelle).
**Bypass vue dossier** : Admin + DG + DSI.
**Composition** : `mon_secteur` reste filtre d’affichage **par-dessus** l’ACL R1 ;
décocher ne redonne plus tout le référentiel.

### `record_habilitations` — réparation

- Colonnes morph **déjà** `habilitable_type` / `habilitable_id` depuis #1075.
  **`record_type` / `record_id` n’ont jamais existé** — log 05/08 = mauvais nom
  de colonne, pas une table cassée.
- Migration `#1288` (`2026_08_12_140000_record_habilitations_historique_1288`) :
  ajoute `retire_par_id`, `retire_le` ; drop unique `record_hab_unique` ;
  index actif `record_hab_actif_idx` ; soft-revoke dans `syncHabilitations`
  (pas de hard delete qui efface l’historique R7).
- Preuve : script section 2 — requête `WHERE habilitable_type = …` (+
  `retire_le IS NULL`) ne plante plus.

### ACL parcelle / propriétaire

- **Parcelle** : `HasFoncierParcelleVisibilite` + `FoncierParcelleVisibiliteScope`
  (R1) — remplace le périmètre « créateur/partage » #1116 sur Parcelle.
- **Propriétaire** : conserve `HasFoncierReferentielVisibilite` (#1116) —
  bloc « propriétaires sans parcelle » Robin 5 / Eric 1.
- Contournement lecture : `Parcelle::queryWithoutParcelleAcl()` /
  `DossierFoncier::queryWithoutRecordAcl()`.

### Refus serveur (cœur du lot)

URL directe dossier + entités rattachées (offres, promesses, avenants, CS,
actes liés, scénarios, documents, commentaires, habilitations) : scope +
policy / authorize. Fiche parcelle + dérivés : binding scope → 404.
Recherches JSON parcelles : passent par le même scope R1.
Inventaire exhaustif : section 7 du script `verif_cloisonnement_foncier_1288.php`
(statique code ; lignes « non/partiel » justifiées — communes search,
patrimoine #1120, corbeille permission, actes sans dossier).

### Mesure (OBJET 5)

Script READ-ONLY : avant = `queryWithoutParcelleAcl` /
`queryWithoutRecordAcl` ; après = Auth + scopes. Branches R1a–R1e peuplées
pour Eric (#35) + lambda. Coût sous-requête paquet R1e en ms. Phrase
chiffrée section 4. À rejouer en prod après migrate.

### Serialisation / dépendances

- **#1286** : retrait parcelle dossier — **arrêt diagnostic** (périmètre
  dérivé, pas de feature) ; ne bloque pas #1288.
- **#1287** : états retenues marchés — **existe** dans le repo (`SUIVI_1287_*`,
  journal) ; hors périmètre foncier, déjà poussé.
- Ne pas toucher CIE / marchés / dépôt factures / `SecteurResolutionService` /
  `Gate::before`.

### Arbitrages retenus / restants

| Sujet | Décision |
|-------|----------|
| Qui **accorde** une autorisation d’usage parcelle (R5) | **Provisoire** : créateur de la parcelle + Admin + DG (aligné R4 dossier). Titulaire secteur non retenu pour l’instant — à confirmer Robin si besoin métier |
| Dossiers **sans** aucune habilitation | Visibles créateur + DG + DSI uniquement (R2). Les lister via script §5a avant déploiement ; pas de backfill auto |
| Paquet via même dossier hors périmètre (4e #1285) | Couvert par **R1c** (dossier habilité) — distinct du paquet propriétaire R1e |

## DÉPLOIEMENT-TEST

```bash
git pull origin main
php artisan migrate --force
php artisan optimize:clear
php artisan journal:sync
php -d display_errors=1 tools/diag/verif_cloisonnement_foncier_1288.php
```

Coller sections 2–6 + 8. Tests écran Robin : usurpation Lahouari (refus),
Eric (dossier + secteur), retrait hab (historique), Admin/DG vue complète,
R4 forge refus, paquet sans rebond, R5 rattachement refusé.

## LEÇON

`record_type` fantôme : un log prod « Column not found » ne signifie pas que
la table est mal migrée — souvent un **mauvais nom** alors que le morph
(`habilitable_*`) est correct depuis le jour 1. Avant d’« ajouter » une
colonne, `SHOW COLUMNS` + grep des requêtes. Soft-revoke (`retire_le`) plutôt
que `sync()` destructif pour respecter un historique d’habilitations (R7).
Fiches lues : #1285 (Auth::setUser, paquet, inventaire), #1286 (arrêt),
#1116 (propriétaires), #1075 (record ACL), #1283 (vue complète).
