---
suivi: 1465
date: 2026-09-07
sujet: Droits avis d'imposition — permission dédiée + menu PATRIMOINE découplé
chantier: droits / patrimoine / avis-imposition
type: feature
statut: poussé
hash: 9952cf42
fichiers:
  - app/Support/Foncier/AvisImpositionDroits.php
  - app/Http/Controllers/Foncier/AvisImpositionController.php
  - app/Http/Middleware/HandleInertiaRequests.php
  - config/feature_permissions.php
  - database/migrations/2026_09_07_120000_add_avis_imposition_permissions.php
  - routes/web.php
  - resources/js/Layouts/AppShell.vue
  - public/build/
  - tests/Unit/AvisImposition1465SourceTest.php
  - tests/Feature/Foncier/AvisImposition1465DroitsFeatureTest.php
  - tests/Unit/AvisImposition1438SourceTest.php
  - tests/Unit/AvisImposition1439SourceTest.php
  - tests/Unit/GsGemmePdgDroits1434SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1465_droits_avis_imposition.md
  - docs/suivi/SUIVI_1463_fiche_parcelle_detentrice_urbanisme.md
---

# SUIVI #1465 — PHASE 2 — droits avis d'imposition (découplage foncier.lecture)

## PROMPT ENVOYÉ

Correction suite #1464 : GS_COMPTABILITE bloqué par middleware
`feature.permission:foncier.lecture` avant `peutAcceder()`. Créer permission
dédiée, sortir les routes avis, découpler le menu PATRIMOINE. Décisions Robin
1–4 (L+E compta, pas suppression, menu avis seul, aucun groupe ne perd).

## BASE GIT

```
git log --oneline -1 origin/main
8482e034 chore(suivi): hash fiche #1464
```

MySQL agent : refusé. Aucun chiffre inventé.

## FICHES LUES

- **#1464** (LEÇON) — porte = middleware foncier.lecture ; menu = même flag ;
  ne pas accorder foncier.lecture à la compta ; helpers hasAnyM365Group /
  Gate::before / is_admin_erp faussement positifs.
- **#1420** — GROUPES_AUTORISES = COMPTABILITE, DG, ADMIN_ERP, GEMME_PDG ;
  GS_HECTARE absent volontairement.
- **#1434** — permission DB indispensable ; GEMME_PDG a foncier.lecture.
- **#1438** — GROUPES_SUPPRESSION sans COMPTABILITE (tient).
- **#1439** — historique était aligné suppression ; **#1465** élargit historique
  lecture à GROUPES_AUTORISES (décision Robin 1).
- **#1422** — dropdown Patrimoine.

Impact sur l’approche : découplage permission + menu (option B4/#1464), pas de
grant foncier.lecture ; historique ≠ suppression.

## SYNTHÈSE

### A. Permission créée — choix lecture + écriture

Catalogue : `avis_imposition.lecture` **et** `avis_imposition.ecriture`
(cohérent avec `foncier.lecture`/`foncier.gestion`, `admin.*.lecture`/`.ecriture`).

- **Middleware de route** = `avis_imposition.lecture` seule (porte d’entrée).
- **Écriture métier** = toujours `peutAcceder()` / GROUPES_AUTORISES (inchangé).
- **`.ecriture`** peuplée pour le catalogue admin et les mappings seed ; non
  utilisée comme middleware (évite double porte et aligne « tout sauf delete »
  sur la barrière groupes).

Migration idempotente `2026_09_07_120000_add_avis_imposition_permissions.php` :
`updateOrInsert` sur `features_permissions` + `feature_permission_groups`,
groupes = `AvisImpositionDroits::GROUPES_AUTORISES`, `flushCache()`.

### Groupes rattachés

| Groupe | Rattaché ? | Justification |
|--------|------------|---------------|
| GS_COMPTABILITE | oui | décision 1 ; était bloqué par foncier.lecture |
| GS_DIRECTION_GENERALE | oui | ∈ GROUPES_AUTORISES ; n’avait pas foncier.lecture en prod |
| GS_ADMIN_ERP | oui | ∈ GROUPES_AUTORISES ; aussi joker `*` PermissionService |
| GS_GEMME_PDG | oui | ∈ GROUPES_AUTORISES + avait foncier.lecture |
| GS_HECTARE | **non** | ∉ GROUPES_AUTORISES (#1420) ; accès effectif déjà 403 contrôleur ; inchangé |
| GS_DSI | **non** (ligne groups) | joker `*` catalogue → permission une fois créée ; menu exige aussi `peutAcceder` → pas de lien 403 ; accès effectif toujours 403 contrôleur |

GROUPES_AUTORISES / GROUPES_SUPPRESSION : **inchangés** (code #1420 / #1438).

### B. Routes

Sorties du groupe `foncier.lecture`. Nouveau groupe :
`feature.permission:avis_imposition.lecture` + `foncier.module` (module enseigne
conservé — avis sous patrimoine/foncier enseigne).

`/patrimoine` parcellaire reste sous `foncier.lecture`.

DELETE soft-delete : même middleware lecture + `peutSupprimer()` sans COMPTABILITE.
Pas de routes corbeille/restauration dédiées avis (soft-delete ligne uniquement).

### C. Menu

- `nav.avis_imposition_visible` = `AvisImpositionDroits::menuVisible`
  (module ∧ permission lecture ∧ GROUPES_AUTORISES).
- PATRIMOINE visible si `foncier_visible` **OU** `avis_imposition_visible`.
- Lien parcellaire : `foncier_visible` seul.
- Lien avis : `avis_imposition_visible`.
- Menu FONCIER : `foncier_visible` seul.
- Une seule entrée pour la compta → pas de séparateur orphelin (pas de `<hr>`).

### D. Mapping routes

| Route | Permission AVANT | Permission APRÈS | Groupes autorisés APRÈS (effectif métier) | Menu | GS_COMPTABILITE |
|-------|------------------|------------------|---------------------------------------------|------|-----------------|
| GET /patrimoine/avis-imposition | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | Avis d'imposition | oui |
| POST …/avis-imposition (store) | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | — | oui |
| PATCH …/{id} | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | — | oui |
| GET …/{id}/historique | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES (#1465) | — | oui |
| POST export excel/pdf | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | — | oui |
| GET recap | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | — | oui |
| POST/GET/DELETE documents | foncier.lecture | avis_imposition.lecture | GROUPES_AUTORISES | — | oui |
| DELETE …/{id} | foncier.lecture + GROUPES_SUPPRESSION | avis_imposition.lecture + GROUPES_SUPPRESSION | GROUPES_SUPPRESSION | — | **non** |
| GET /patrimoine (parcellaire) | foncier.lecture | foncier.lecture (inchangé) | détenteurs foncier.lecture | Patrimoine | **non** |

### Alignement menu ↔ route (aucune entrée → 403)

| Entrée menu | Condition visibilité | Garde route | Aligné |
|-------------|---------------------|-------------|--------|
| PATRIMOINE → Patrimoine (parcellaire) | `nav.foncier_visible` | `foncier.lecture` + `foncier.module` | oui |
| PATRIMOINE → Avis d'imposition | `nav.avis_imposition_visible` (= permission ∧ module ∧ GROUPES_AUTORISES) | `avis_imposition.lecture` + `foncier.module` + `peutAcceder` | oui |
| Menu FONCIER (toutes entrées) | `nav.foncier_visible` | `foncier.lecture` (+ permissions spécifiques) | oui (inchangé) |

### Requête de contrôle post-migration

```sql
SELECT fp.feature_key, fp.libelle, fpg.groupe_m365
FROM features_permissions fp
LEFT JOIN feature_permission_groups fpg
  ON fpg.feature_permission_id = fp.id
WHERE fp.feature_key LIKE 'avis_imposition.%'
ORDER BY fp.feature_key, fpg.groupe_m365;
```

Attendu : 2 feature_key × 4 groupes (COMPTABILITE, DIRECTION_GENERALE,
ADMIN_ERP, GEMME_PDG) = 8 lignes de rattachement (+ éventuel joker DSI/ADMIN
non listé dans feature_permission_groups).

### Comptes de test (Robin)

- Rudy BENOR / GS_COMPTABILITE — index + édition OK ; suppression refusée ; menu
  FONCIER absent ; parcellaire absent ; avis visible.
- Sabrina BARTOLI / compta — même matrice suppression.
- Sandrine PORTALES / GS_GEMME_PDG — accès préservé + suppression OK.
- Compte GS_HECTARE — avis toujours refusé (GROUPES_AUTORISES).
- Ne pas valider avec le compte GS_ADMIN_ERP (Robin) : joker Gate / helpers.

## DÉPLOIEMENT / TEST

```bash
git pull origin main
php artisan migrate --force
find app/ config/ database/ -name "*.php" -exec touch {} +
php artisan route:clear
php artisan view:clear
php artisan optimize:clear
php artisan journal:sync
```

Puis exécuter la requête SQL de contrôle. Cache ACL / Graph ~5 min : un 403
immédiat post-déploiement n’est pas forcément un bug.

JAMAIS npm run build / composer install / optimize / config:cache sur OVH.

## LEÇON

1. Accès métier (`GROUPES_AUTORISES` + `m365GroupNames` brut) ≠ porte middleware
   catalogue — les deux doivent exister et être alignés au menu (#1434 + #1464).
2. Ne jamais tester les droits avec GS_ADMIN_ERP / `hasAnyM365Group` /
   `Gate::before` / `auth.is_admin_erp`.
3. Découpler une sous-fonction du module parent évite d’ouvrir tout le foncier
   à la compta.
4. Historique lecture peut diverger de la suppression (#1465 vs #1439).
5. Tailwind v4 scanne `docs/suivi/` : ne pas laisser de classes arbitraires
   littérales type `min-[…]:…` dans la prose (récidive #1463).
