---
suivi: 1578
date: 2026-09-16
sujet: Opposition propriétaire + alerte d’identité à la création
chantier: foncier / proprietaires / rgpd
type: feature
statut: poussé
hash: 2cc86c1f
fichiers:
  - database/migrations/2026_09_16_170000_add_opposition_to_proprietaires.php
  - app/Models/Proprietaire.php
  - app/Services/Foncier/ProprietaireIdentiteRapprochementService.php
  - app/Http/Controllers/Foncier/ProprietaireController.php
  - app/Http/Controllers/Foncier/ParcelleController.php
  - app/Http/Controllers/Foncier/DossierFoncierController.php
  - app/Http/Controllers/Foncier/ProspectionController.php
  - app/Services/Foncier/ParcelleProspectionQuery.php
  - app/Services/Foncier/ProprietaireListeQuery.php
  - app/Http/Middleware/HandleInertiaRequests.php
  - routes/web.php
  - resources/js/Components/Foncier/OppositionProprietaireBandeau.vue
  - resources/js/Components/Foncier/ProprietaireCandidatsAlerte.vue
  - resources/js/Pages/Foncier/Proprietaires/Show.vue
  - resources/js/Pages/Foncier/Proprietaires/Form.vue
  - resources/js/Pages/Foncier/Proprietaires/Index.vue
  - resources/js/Pages/Foncier/Parcelles/Show.vue
  - resources/js/Pages/Foncier/Dossiers/Show.vue
  - resources/js/Pages/Foncier/Dossiers/Form.vue
  - resources/js/Components/Foncier/DossierFoncierTab.vue
  - resources/js/Pages/Programmes/Foncier.vue
  - resources/js/Pages/Foncier/Prospection/Index.vue
  - resources/js/Components/Foncier/Prospection/ProspectionParcellesSublist.vue
  - tests/Unit/Proprietaire1578SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1578_opposition_proprietaire.md
---

# SUIVI #1578 — Opposition propriétaire + unicité à la création

## PROMPT ENVOYÉ

Marqueur d’opposition personne-wide (`opposition_at` / `opposition_par_id`)
sur le patron #1563 / #1574. Bandeau rouge partout où le propriétaire
apparaît. Pas de blocage. Alerte d’identité à la création (rattacher ou
créer quand même). Pas de contrainte d’unicité en base. Hors périmètre :
Bloctel, consentement téléphonique, fil typé, purge 5 ans, fusion.

## FICHES LUES

- **#1576 LEÇON** — l’opposition n’existe pas ; le bon support est la
  personne `proprietaires`, à condition de traiter les doublons à la
  création. `rgpd_consent` n’est pas une opposition. Impact : marqueur
  sur `proprietaires`, pas sur la parcelle ; `store()` n’insère plus à
  l’aveugle.
- **#1574 / #1563 LEÇON** — timestamp + auteur, hors `$fillable`,
  `forceFill`, pas de booléen nu, audit via `auditedFields`. Impact :
  même patron (`opposition_at` / `opposition_par_id`).
- Aucune fiche n’imposait un écran de fusion : hors périmètre confirmé.

## DROITS RETENUS (avant code)

Poser / retirer une opposition = **même porte que la modification** du
propriétaire : permission `foncier.gestion`.

Lecture brute de `m365GroupNames()` via `PermissionService` (pas
`Gate::`, pas `hasAnyM365Group`) :

| Groupe M365 | `foncier.gestion` |
|---|---|
| `GS_HECTARE` | oui (migration `2026_07_30_220000` + `config/feature_permissions.php`) |
| `GS_DSI` | oui (catalogue entier) |
| `GS_ADMIN_ERP` | oui (catalogue sauf `admin.systeme` / `admin.maintenance`) |
| `GS_ENVOL` | non (`!foncier.*`) |
| `GS_GEMME_PDG` | non (lecture / prix / déverrouillage seulement) |

Contrôle serveur : middleware `feature.permission:foncier.gestion` sur
les routes POST/DELETE + `abort_unless` dans
`poserOpposition` / `retirerOpposition` / `assertGestion`. Un `v-if`
seul n’écrit pas.

## MAPPING UI ↔ BACK — opposition

| Libellé affiché | Valeur postée | Colonne DB | Libellé back |
|---|---|---|---|
| Ce propriétaire s’oppose à être recontacté | POST `/foncier/proprietaires/{id}/opposition` | `opposition_at`, `opposition_par_id` | Opposition au recontact / auteur |
| (décocher) | DELETE même URL | `opposition_at` = null, `opposition_par_id` = null | (retrait audité) |

Hors `$fillable`. Écriture `forceFill` uniquement.

## MAPPING UI ↔ BACK — rapprochement

| Libellé affiché | Valeur postée | Colonne DB | Libellé back |
|---|---|---|---|
| Rattacher celui-ci | id existant (attach / visite fiche) | `proprietaires.id` existant | — |
| Créer quand même | `force_create=1` | nouvelle ligne `proprietaires` | — |

Règle : physique nom+prénom (casse/accents via
`FournisseurDoublonDetector::normalizeNom`) ; morale raison sociale ou
SIRET ; e-mail ou téléphone (tout type). Pas d’unicité SQL.

## ÉCRANS — BANDEAU

Bandeau rouge, visible sans action :

- fiche propriétaire (`Proprietaires/Show.vue`)
- fiche parcelle (`Parcelles/Show.vue`) — un bandeau par détenteur
  actuel ou ancien sous opposition
- fiche dossier (`Dossiers/Show.vue`) — hors onglet, toujours visible
- onglet FONCIER (`DossierFoncierTab.vue`) et hub programme
  (`Programmes/Foncier.vue`)

Marqueur `Opp.` sur la ligne (pas un bandeau) :

- liste parcelles Prospection (`Index.vue` + `ProspectionParcellesSublist.vue`)
- mode propriétaire de la même liste
- liste propriétaires (`Proprietaires/Index.vue`)
- nom du groupe dans `DossierFoncierTab`

## SYNTHÈSE

Marqueur date+auteur, bandeau informatif, alerte d’identité sans
blocage. Bloctel, fil typé, purge et fusion non touchés.

## DÉPLOIEMENT-TEST

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

## LEÇON

L’opposition personne-wide est inopérante si `store()` duplique la
fiche. L’alerte à la création (exacte, pas fuzzy #1194) est le vrai
garde-fou, pas une contrainte UNIQUE (homonymes légitimes). Un bandeau
sur la fiche seule ne suffit pas : le courrier part depuis la parcelle
et le dossier. `Gate::before` (GS_ADMIN_ERP) fausse tout test Robin :
droits lus via `PermissionService` + `m365GroupNames()` brut.
