---
suivi: 1464
date: 2026-09-07
sujet: Diagnostic droits avis d'imposition — GS_COMPTABILITE vs foncier.lecture + menu
chantier: droits / patrimoine / avis-imposition
type: diagnostic
statut: poussé
hash: 09077a09
fichiers:
  - docs/suivi/SUIVI_1464_diag_droits_avis_imposition.md
---

# SUIVI #1464 — PHASE 1 DIAGNOSTIC — droits avis d'imposition

## PROMPT ENVOYÉ

PHASE 1 lecture seule. Deux défauts : (1) `/patrimoine/avis-imposition` bloqué
par middleware `foncier.lecture` pour Rudy BENOR / GS_COMPTABILITE ; (2) menu
PATRIMOINE invisible (même cause). Retrouver l’étude préalable ; confronter
décisions Robin 1B (compta L+E + corbeille) et 2A (menu PATRIMOINE = avis seul).
Aucun code, aucune migration, aucun rebuild. Aucun chiffre inventé.

## BASE GIT

```
git log --oneline -1 origin/main
808294e1 chore(suivi): hash fiche #1463
```

MySQL local : refusé. Aucun navigateur. Requêtes fournies pour Robin.

## FICHES / DOCS LUS

- **#1420** — socle avis ; `GROUPES_AUTORISES` via `m365GroupNames()` brut.
- **#1422** LEÇON — menu PATRIMOINE dropdown ; accès = `peutVoirMenuFoncier`.
- **#1434** LEÇON — sans `foncier.lecture` en DB, menu + routes `/patrimoine`
  restent 403 malgré `GROUPES_AUTORISES` ; grant GEMME_PDG.
- **#1438** — `GROUPES_SUPPRESSION` exclut **volontairement** GS_COMPTABILITE.
- **#1439** — historique = mêmes groupes que suppression (compta exclu).
- **#1207** — Patrimoine top-level, même middleware que Foncier.
- **#1235** / `docs/droits/RAPPORT_DROITS_ERP.md` — matrice générale ; **pas**
  de section dédiée avis d’imposition.
- `PROJECT.md` / `docs/foncier` / `docs/audit` : **rien** sur cette matrice avis.

---

## EN TÊTE — ÉTUDE PRÉALABLE RETROUVÉE

**Pas de document unique** intitulé « étude droits avis d’imposition » dans
`docs/suivi/`, `docs/droits/`, `docs/audit/`, `PROJECT.md`.

**Étude reconstituée** à partir des décisions **codées et fichéées** qui font
autorité sur ce module :

| Lot | Décision arrêtée |
|-----|------------------|
| **#1420** | Accès métier avis = liste en dur `GROUPES_AUTORISES` : `GS_COMPTABILITE`, `GS_DIRECTION_GENERALE`, `GS_ADMIN_ERP`, `GS_GEMME_PDG`. Contrôle `m365GroupNames()` brut (pas Gate / `hasAnyM365Group`). **GS_HECTARE / GS_ENVOL absents** → « le foncier n’a pas spécialement accès aux avis d’impôt » = vrai au sens contrôleur. |
| **#1434** | Pour atteindre les routes `/patrimoine/*`, il faut **en plus** `foncier.lecture` (middleware + menu). GEMME_PDG reçoit `foncier.lecture` (+ prix, deverrouillage) par migration DB. LEÇON : config `default_mappings` ≠ runtime (`feature_permission_groups`). |
| **#1438** | Corbeille (soft-delete ligne) réservée à `GROUPES_SUPPRESSION` = GEMME_PDG, DIRECTION_GENERALE, ADMIN_ERP. **GS_COMPTABILITE exclu**. Comptes de refus : Sabrina BARTOLI / Rudy BENOR. |
| **#1439** | Consultation historique = même liste que suppression (compta exclu). Pas de restauration. |
| **#1422** | Menu : dropdown Patrimoine sous `peutVoirMenuFoncier` (= `nav.foncier_visible`). |

### Matrice retenue (état code actuel)

| Action | Condition |
|--------|-----------|
| Atteindre toute route `/patrimoine/avis-imposition*` | Middleware `feature.permission:foncier.lecture` **puis** `foncier.module` **puis** `peutAcceder()` ∈ GROUPES_AUTORISES |
| Voir menus FONCIER **et** PATRIMOINE | `foncier.lecture` + module enseigne actif (`FoncierContext::menuVisible`) |
| Créer / patch inline / docs / exports / récap | `peutAcceder()` (GROUPES_AUTORISES) ; UI `peut_modifier: true` hardcodé si index OK |
| Soft-delete (corbeille ligne) | `peutSupprimer()` ∈ GROUPES_SUPPRESSION (**sans** COMPTABILITE) |
| Historique ligne | idem suppression |

### Confrontation décisions Robin 1B / 2A

| Cible Robin | Étude / code actuel | Statut |
|-------------|---------------------|--------|
| **1B** Compta L+E **y compris corbeille** | #1438/#1439 : compta **exclue** de corbeille + historique | **CONTRADICTION** — ne pas trancher ; Robin arbitrera |
| **1B** Compta lecture/écriture champs | #1420 : COMPTABILITE ∈ GROUPES_AUTORISES (intention) mais **bloquée avant** par `foncier.lecture` | Aligné sur l’intention #1420 **si** la porte middleware/menu est séparée |
| **2A** Menu PATRIMOINE = avis seul, pas le reste foncier | Aujourd’hui PATRIMOINE et FONCIER partagent `peutVoirMenuFoncier` ; grant `foncier.lecture` ouvrirait **aussi** le menu FONCIER + toutes les routes foncier | **Risque majeur** ; 2A **faisable** seulement en **découplant** menu/routes avis de `foncier.lecture` (voir B4 / E) |

**Clause d’arrêt (partielle)** : contradiction 1B ↔ #1438 sur la corbeille ; impact large si on se contente d’accorder `foncier.lecture` à GS_COMPTABILITE. Diagnostic continue en cartographiant ; **aucune recommandation d’accorder `foncier.lecture` à la compta**.

---

## A. LA PERMISSION ACTUELLE

### A1. Où `foncier.lecture` est exigé pour `/patrimoine/avis-imposition`

**Middleware de route** (avant le contrôleur), pas une Policy Gate sur cet écran.

```
// routes/web.php L716–724
Route::middleware(['feature.permission:foncier.lecture', 'foncier.module'])
    ->group(function (): void {
        Route::get('/patrimoine', ...);
        Route::get('/patrimoine/avis-imposition', [AvisImpositionController::class, 'index'])
            ->name('foncier.patrimoine.avis-imposition.index');
```

`EnsureFeaturePermission` (`app/Http/Middleware/EnsureFeaturePermission.php` L17–39) :
si échec → Inertia `Errors/Forbidden` avec
`message` = « Vous n'avez pas accès à cette fonctionnalité. » et
`feature_key` = `foncier.lecture` — **exactement** le constat Rudy.

Ensuite seulement : `AvisImpositionController::index` L67
`abort_unless(... peutAcceder($user) ...)` (groupes M365).

Rudy échoue au **premier** verrou (middleware) ; `peutAcceder` n’est jamais atteint.

### A2. Définition de `foncier.lecture` et rattachement groupes

- **Catalogue source** : `config/feature_permissions.php` L145
  `'foncier.lecture' => ['Foncier', 'Foncier — lecture', ...]`.
- **Runtime** : tables `features_permissions` + `feature_permission_groups`
  (`FeaturePermission` L10–11 ; `PermissionService` L56–60 lit la DB, cache 5 min).
- **Seed mappings config** (amorçage, ≠ vérité prod) :
  - `GS_HECTARE` → `foncier.lecture` (+ gestion, prix) L290–294
  - `GS_GEMME_PDG` → `foncier.lecture` (+ prix, deverrouillage) L299–303 (#1434)
  - `GS_COMPTABILITE` → **aucun** `foncier.*` L241–259
  - `GS_DIRECTION_GENERALE` → `!foncier.*` L186–188
  - `GS_ADMIN_ERP` / `GS_DSI` → joker `*` (PermissionService L45–54)

Écran admin : `/admin/droits` (permissions `admin.droits.lecture` / `.ecriture`).

#### Requêtes (coller en prod)

```sql
-- A2.a — groupes rattachés à foncier.lecture (et autres foncier.*)
SELECT fp.feature_key, fpg.groupe_m365
FROM features_permissions fp
JOIN feature_permission_groups fpg ON fpg.feature_permission_id = fp.id
WHERE fp.feature_key LIKE 'foncier.%'
ORDER BY fp.feature_key, fpg.groupe_m365;

-- A2.b — existe-t-il déjà une permission avis_* ?
SELECT feature_key, libelle, categorie
FROM features_permissions
WHERE feature_key LIKE '%avis%' OR libelle LIKE '%avis%' OR libelle LIKE '%imposition%';
```

### A3. Permission dédiée avis ?

**NON** dans le catalogue config (L144–151 : seulement `foncier.*`).
Contrôle métier = constantes PHP `GROUPES_AUTORISES` / `GROUPES_SUPPRESSION`
dans `AvisImpositionController` L40–60.

Pourquoi la route n’utilise pas une permission dédiée : le socle #1420 a placé
les routes dans le **même** groupe middleware que `/patrimoine` / Foncier
(#1207 « même middleware »), puis a ajouté un **second** filtre groupes.

**Faut-il la créer ?** Proposition E : **oui**, pour réaliser 2A sans ouvrir le
foncier à la compta — **sous réserve d’arbitrage Robin** (et du sort de la
corbeille vs #1438).

### A4. Tableau routes / actions / permission actuelle

Toutes sous `feature.permission:foncier.lecture` + `foncier.module`
(`routes/web.php` L716–754), puis `peutAcceder()` sauf précision.

| Route | Méthode | Action | Middleware | Contrôle contrôleur |
|-------|---------|--------|------------|---------------------|
| `/patrimoine/avis-imposition` | GET | index (liste) | foncier.lecture + module | `peutAcceder` |
| `/patrimoine/avis-imposition` | POST | store (ligne vide) | idem | `peutAcceder` |
| `/patrimoine/avis-imposition/{id}` | PATCH | update champ inline | idem | `peutAcceder` |
| `/patrimoine/avis-imposition/{id}` | DELETE | soft-delete (« corbeille ») | idem | `peutAcceder` **+** `peutSupprimer` (GROUPES_SUPPRESSION) |
| `/patrimoine/avis-imposition/{id}/historique` | GET | historique | idem | `peutAcceder` **+** `peutConsulterHistorique` (= suppression) |
| `/patrimoine/avis-imposition/{id}/documents` | POST | ajout PJ | idem | `peutAcceder` |
| `.../documents/{document}/download` | GET | download PJ | idem | `peutAcceder` |
| `.../documents/{document}` | DELETE | supprimer PJ | idem | `peutAcceder` (pas GROUPES_SUPPRESSION — #1438 : croix doc reste sur `peut_modifier`) |
| `/patrimoine/avis-imposition/export/excel` | POST | export Excel | idem | `peutAcceder` |
| `/patrimoine/avis-imposition/export/pdf` | POST | export PDF | idem | `peutAcceder` |
| `/patrimoine/avis-imposition/recap` | GET | récap | idem | `peutAcceder` |

**Pas** de route restore / forceDelete / écran corbeille dédié (soft delete
uniquement ; pas de `restore` dans le contrôleur).

UI : `peut_modifier` toujours `true` à l’index si accès OK (L80) ;
`peut_supprimer` / `peut_historique` selon groupes.

### A5. Lien avec #1438 « corbeille absente pour GS_COMPTABILITE »

**Même famille, deux couches :**

1. **#1438** : même si la page s’ouvre, le bouton corbeille est masqué /
   `destroy` 403 pour COMPTABILITE (`GROUPES_SUPPRESSION`, preuve
   `AvisImpositionController` L55–59, L194–196 ; fiche #1438 L86–94).
2. **#1464 (constat Rudy)** : la page **ne s’ouvre pas du tout** faute de
   `foncier.lecture` (middleware) + menu invisible.

Cause racine du **403 page** ≠ exclusion corbeille #1438, mais **prérequis
middleware partagé**. L’exclusion corbeille #1438 reste un **choix métier
volontaire** distinct — en tension avec la cible 1B actuelle.

---

## B. LE MENU

### B1. Construction

`resources/js/Layouts/AppShell.vue` :
- `peutVoirMenuFoncier` = `page.props.nav.foncier_visible` (L677–679)
- Bloc PATRIMOINE : `v-if="peutVoirMenuFoncier"` (L1613–1615)
- Sous-entrées : Lien « Patrimoine » → `/patrimoine` ; « Avis d'imposition » →
  `/patrimoine/avis-imposition` (L1644–1665)

Share Inertia : `HandleInertiaRequests.php` L91–93 →
`FoncierContext::menuVisible($user)`.

```
// app/Support/FoncierContext.php L33–36
public static function menuVisible(?User $user): bool
{
    return self::userPeutLire($user) && self::moduleActif();
}
// userPeutLire = PermissionService::userHasPermission(..., 'foncier.lecture') L24–30
```

### B2. Même permission que la route ?

**Oui pour la porte d’entrée** : menu et routes reposent sur `foncier.lecture`
(+ module enseigne).

**Divergence ensuite** : le contrôleur ajoute `GROUPES_AUTORISES`. Donc :
- Compta : dans GROUPES_AUTORISES, **sans** lecture → ni menu ni route (défaut double).
- Hectare : a lecture (seed), **hors** GROUPES_AUTORISES → menu FONCIER/PATRIMOINE
  visibles, mais avis → 403 `peutAcceder` (si on clique).

C’est bien la source du double défaut compta + le piège hectare.

### B3. Entrées menu PATRIMOINE et conditions

| Entrée | Href | Condition de visibilité actuelle |
|--------|------|----------------------------------|
| Bouton top-level « Patrimoine » | (dropdown) | `peutVoirMenuFoncier` (= `foncier.lecture` ∧ module) |
| Sous-entrée « Patrimoine » | `/patrimoine` | **aucune** sous-condition (affichée dès le dropdown) |
| Sous-entrée « Avis d'imposition » | `/patrimoine/avis-imposition` | **aucune** sous-condition |

Même `v-if` que le menu **FONCIER** (L1135).

### B4. Faisabilité option 2A (PATRIMOINE avec une seule sous-entrée)

**État actuel** : tout-ou-rien **couplé au menu FONCIER** via le même flag.

**Techniquement** : **OUI**, on peut afficher un dropdown PATRIMOINE avec
**seulement** « Avis d'imposition » (et masquer le lien « Patrimoine » + ne
pas activer FONCIER) — en introduisant un flag séparé
(ex. `nav.avis_imposition_visible`) et des `v-if` par `Link`. Ce n’est **pas**
impossible ; ce n’est **pas** non plus un simple grant `foncier.lecture`.

Accorder `foncier.lecture` à COMPTABILITE **sans** découplage = ouvrir FONCIER
+ `/patrimoine` (parcelles patrimoine) + toutes routes `foncier.*` middleware —
**contredit 2A**.

### B5. Alignement menu ↔ garde route

Après correction, la condition d’affichage de l’entrée « Avis d'imposition »
**doit** être la même que la garde de route (permission dédiée **ou**
`peutAcceder` exposé en nav), pour ne pas proposer un lien 403.
Aujourd’hui : menu = lecture seule ; route = lecture **+** groupes → déjà
désaligné pour Hectare.

---

## C. PIÈGE MAJEUR (helpers admin)

### C1. Passage par `hasAnyM365Group` / `Gate::before` ?

**Contrôle avis (contrôleur)** : **NON** — `m365GroupNames()` brut
(`peutAcceder` L428–431 ; commentaires L36–37, L50–51). Modèle sain (#1411).

**Middleware `feature.permission`** : `PermissionService` (groupes → DB). Pour
`GS_ADMIN_ERP`, **toutes** les clés catalogue (sauf systeme/maintenance) —
effet « tout passe » **sans** passer par `Gate::before`.

`Gate::before` (`AppServiceProvider` L69–75) et `User::hasAnyM365Group`
(L146–152 : short-circuit `isAdminERP`) : **non utilisés** sur la chaîne avis.

Front menu PATRIMOINE : **pas** `hasM365Groupe` — uniquement `foncier_visible`
(permissions). `useAuthM365().hasM365Groupe` court-circuite admin ailleurs dans
AppShell (Compta, etc.) — hors avis.

➡️ Pas un défaut « Gate::before sur avis » ; le défaut est le **couplage
middleware/menu à `foncier.lecture`**.

### C2. Peuplement groupes — requête

État connu rappelé par le prompt (à confirmer, **ne pas inventer ici**).

```sql
-- C2 — membres GS_COMPTABILITE (JSON array cast)
SELECT u.id, u.name, u.email, u.m365_groups
FROM users u
WHERE JSON_CONTAINS(u.m365_groups, '"GS_COMPTABILITE"')
ORDER BY u.name;

SELECT COUNT(*) AS n_compta
FROM users u
WHERE JSON_CONTAINS(u.m365_groups, '"GS_COMPTABILITE"');
```

(Si `m365_groups` stocké autrement, adapter ; colonne cast `array` sur `User`.)

### C3. Comptes de test (après fix) — nominatifs issus des fiches

| Objectif | Compte | Groupe |
|----------|--------|--------|
| Accès OK (hors admin) | **Rudy BENOR** (`rudy.benor@hectaregroupe.fr`) ou **Sabrina BARTOLI** | GS_COMPTABILITE (#1438) |
| Accès OK direction/PDG | **Sandrine PORTALES** | GS_GEMME_PDG (#1434) |
| Refus toujours effectif (ne doit **pas** accéder aux avis) | Compte **GS_HECTARE seul** (métier foncier hors liste GROUPES_AUTORISES) — **nom exact à confirmer par Robin** via requête membres Hectare sans chevauchement COMPTABILITE/DG/ADMIN | |
| Refus admin inutilisable | Robin / GS_ADMIN_ERP | court-circuit permissions — **ne pas** s’en servir pour un test de refus |

### C4. Propagation

Après changement groupe M365 ou `/admin/droits` :
`PermissionService` cache **300 s** (`CACHE_TTL_SECONDS` L20).
Un 403 immédiat après rattachement ≠ bug. Rappeler : attendre ~5 min ou
`PermissionService::flushCache` / reconnect ; ingestion Graph au login.

---

## D. IMPACT ET NON-RÉGRESSION

### D1. Qui a accès AUJOURD’HUI ? (formule, pas de chiffre)

**Formule code** : utilisateur U accède à la page ssi

1. `PermissionService` accorde `foncier.lecture` à U, **et**
2. module foncier actif sur une enseigne, **et**
3. `array_intersect(GROUPES_AUTORISES, U.m365GroupNames())` non vide.

Groupes **candidats** GROUPES_AUTORISES : COMPTABILITE, DIRECTION_GENERALE,
ADMIN_ERP, GEMME_PDG — **sous réserve** qu’ils aient aussi `foncier.lecture`
en DB (ADMIN/DSI oui ; GEMME_PDG oui via #1434 ; COMPTABILITE **non** seed ;
DG seed `!foncier.*` — **vérifier prod**).

```sql
-- D1 — groupes ayant foncier.lecture en DB
SELECT fpg.groupe_m365
FROM features_permissions fp
JOIN feature_permission_groups fpg ON fpg.feature_permission_id = fp.id
WHERE fp.feature_key = 'foncier.lecture'
ORDER BY 1;

-- D1.bis — utilisateurs à l'intersection (lecture + groupe autorisé avis)
-- à croiser en feuille de calcul : membres des 4 GS × porteurs de foncier.lecture
```

### D2. Effet de bord si on ouvre via `foncier.lecture` à COMPTABILITE

**Risque principal — OUI, large et non maîtrisé** :

Le même middleware enveloppe **tout** le préfixe `foncier` (`routes/web.php`
L757+) **et** `/patrimoine` (vue patrimoine parcelles L719). Accorder
`foncier.lecture` à COMPTABILITE ouvrirait :
- menu **FONCIER** (dossiers, prospection, actes, …) — au moins la navigation ;
- `/patrimoine` (patrimoine foncier #1120) ;
- avis (enfin) via `peutAcceder`.

Policies / écrans peuvent encore restreindre certaines actions (`foncier.gestion`),
mais **contredit 2A** et expose un module métier non demandé.

➡️ **Ne pas** proposer ce raccourci comme fix.

### D3. Si permission dédiée : préserver les accès actuels

Groupes à rattacher à la nouvelle clé (lecture / écriture selon arbitrage) pour
**ne perdre personne** qui passe aujourd’hui les trois filtres :

- Ceux déjà dans GROUPES_AUTORISES **et** ayant `foncier.lecture` en prod
  (mesurer D1) : typiquement **GS_ADMIN_ERP**, **GS_GEMME_PDG**, éventuellement
  **GS_DIRECTION_GENERALE** si lecture accordée hors seed.
- **GS_COMPTABILITE** : à ajouter (cible 1B) pour lecture/écriture champs.
- **Ne pas** ajouter GS_HECTARE / GS_ENVOL (respect étude « foncier pas
  spécialement »), sauf arbitrage contraire.

Suppression / historique : **garder** GROUPES_SUPPRESSION **ou** élargir à
COMPTABILITE **selon arbitrage 1B vs #1438**.

### D4. Visibilité par entité / société / périmètre ?

**Non** constaté sur le module avis : `index` charge
`AvisImposition::query()->...->get()` sans filtre enseigne / entité / ACL
dossier (`AvisImpositionController` L69–75). Une fois passé `peutAcceder`,
vision **globale** de la table.

---

## E. REMÉDIATION — PROPOSITION (ne pas exécuter)

### E1. Fichiers (proposition)

| Fichier | Rôle |
|---------|------|
| `config/feature_permissions.php` | Ajouter `avis_imposition.lecture` (+ éventuellement `.ecriture`) au catalogue ; mappings seed COMPTABILITE / DG / ADMIN / GEMME_PDG |
| Migration idempotente (grant DB) | Peupler `features_permissions` + `feature_permission_groups` (pattern #1434) |
| `routes/web.php` | **Sortir** les routes avis du groupe `foncier.lecture` ; middleware `feature.permission:avis_imposition.lecture` (ou lecture,ecriture selon découpe) — **sans** toucher au groupe `/patrimoine` parcellaire ni `/foncier/*` |
| `AvisImpositionController` | Aligner `peutAcceder` / `peut_modifier` / `peutSupprimer` sur la nouvelle permission **ou** conserver GROUPES_* en brut pour écriture/suppression — **à trancher** ; ne pas réintroduire `hasAnyM365Group` |
| `HandleInertiaRequests` + éventuellement `FoncierContext` ou helper dédié | Exposer `nav.avis_imposition_visible` (permission dédiée ∧ module si besoin) |
| `AppShell.vue` | `v-if` PATRIMOINE / sous-entrée avis sur le nouveau flag ; **ne pas** lier à `peutVoirMenuFoncier` ; masquer sous-lien « Patrimoine » pour les profils avis-only (2A) |
| Tests source + feature refus | Voir E4 |
| Journal + fiche SUIVI fix | Impact user visible |

**Ne pas toucher** : #1463 Show parcelle, #1461/#1460/#1458, PatrimoineFoncierQuery métier.

### E2. Migration ?

**Oui** si permission catalogue + rattachements DB (PermissionService ne lit pas
que la config — LEÇON #1434). Pattern
`2026_09_04_100000_grant_foncier_permissions_to_gs_gemme_pdg.php` :
`updateOrInsert`, pas de `Schema::create` avec `->after()`, pas de DDL dans
transaction inutile. Flush cache permissions en fin de `up()`.

### E3. Un ou deux commits ?

**Un seul commit feature** possible et préférable : routes + permission + menu
doivent rester **alignés** (B5). Découper permission sans menu = lien 403 ;
menu sans route = inverse. Éventuel 2ᵉ chore hash fiche.

### E4. Tests de refus (comptes non-admin)

1. **Rudy BENOR** (COMPTABILITE) : GET avis → **200** après fix ; menu PATRIMOINE
   visible avec **Avis** ; lien Patrimoine parcellaire **absent** (2A) ; FONCIER
   **absent**.
2. **Rudy** : PATCH champ → 200 ; DELETE ligne → **200 ou 403** selon arbitrage
   1B vs #1438 (écrire le test **après** arbitrage).
3. Compte **GS_HECTARE seul** : GET avis → **403** ; pas d’entrée avis (ou 403
   si menu foncier reste).
4. **Sandrine PORTALES** (GEMME_PDG) : régression **200** + suppression OK.
5. Jamais de test de refus avec compte GS_ADMIN_ERP / Robin.
6. Source tests : absence `hasAnyM365Group` / `Gate::` sur la chaîne avis ;
   routes avis **hors** groupe middleware `foncier.lecture`.

---

## POINTS RESTANT À ARBITRER (Robin)

1. **Corbeille + historique pour COMPTABILITE** : confirmer 1B (contredit #1438/#1439)
   ou maintenir l’exclusion.
2. **Permission unique** `avis_imposition.lecture` (tout sauf delete) vs
   couple lecture/écriture.
3. **DG** : a-t-elle `foncier.lecture` en prod hors seed ? Mesure D1.
4. Nom du compte **refus Hectare** pour les tests.
5. Faut-il aussi un écran / restore soft-deletes ? (hors constat actuel — pas de
   route).

## NON OBSERVÉ

- Contenu réel `feature_permission_groups` / effectifs users (requêtes fournies).
- Rendu navigateur / impersonation (mesures Robin déjà fournies pour Rudy).

## DÉPLOIEMENT-TEST (ce lot diagnostic)

```
git pull origin main
```

Pas d’autre commande.

## LEÇON

`GROUPES_AUTORISES` sans porte d’entrée dédiée est un droit **mort** : la compta
est « autorisée » dans le contrôleur depuis #1420 mais **n’atteint jamais** la
méthode à cause de `foncier.lecture` (#1434 l’avait déjà formulé pour GEMME_PDG).
Corriger en accordant `foncier.lecture` à la compta casserait 2A ; il faut une
**permission + menu + routes** découplés du module Foncier.
