---
suivi: 1582
date: 2026-09-16
sujet: Interrupteur commercialisation en cours sur programme
chantier: programmes / commercialisation
type: feature
statut: poussé
hash: d8af9468
fichiers:
  - database/migrations/2026_09_16_200000_add_commercialisation_active_to_programmes.php
  - app/Support/ProgrammeCommercialisation.php
  - app/Models/Programme.php
  - app/Http/Controllers/ProgrammeController.php
  - app/Http/Controllers/ProgrammeDashboardController.php
  - routes/web.php
  - resources/js/Pages/Programmes/Dashboard.vue
  - resources/js/Pages/Programmes/Index.vue
  - tests/Unit/Programme1582CommercialisationSourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1582_commercialisation_programme.md
---

# SUIVI #1582 — Commercialisation en cours (programme)

## PROMPT ENVOYÉ

Un interrupteur « Commercialisation en cours » sur la fiche programme.
Étape déduite du croisement interrupteur × terrain acté. Colonne +
filtre SQL sur la liste. Pas de masquage d’onglets, pas de lien avec
la chronologie, pas de modification du taux de lots vendus.

## FICHES LUES

- **#1556 LEÇON** — `dossiers_fonciers.programme_id` UNIQUE, 0 ou 1.
  Soft-delete programme = NULL la FK. Impact : chemin terrain acté =
  ce FK, pas un champ programme. Pas de dossier = terrain non acté.
- **#1569** — `parcelles.statut` déduit, `detenue` dès
  `societe_detentrice_id` NOT NULL (persisté). Impact : lire
  `parcelles.statut = detenue`, ne pas recalculer ni écrire le foncier.
- **#1578 / #1574 / #1563** — patron timestamp + `_par_id`, hors
  `$fillable`, `forceFill`, audit, pas de booléen nu. Impact : recopier
  ce schéma, interrupteur **désactivable** (contrairement à certaines
  phases).
- **#1093** — `Programmes/Index.vue` zone sérielle, grille CSS.
  Impact : rebase origin/main avant Vite ; filtre en query SQL, pas JS.

## CHEMIN « TERRAIN ACTÉ » (établi avant code)

```
programmes.id
  → dossiers_fonciers.programme_id   (#1556, HasOne, UNIQUE)
  → dossier_foncier_parcelle         (périmètre dossier #1289)
  → parcelles.statut = 'detenue'     (#1569, FoncierReferentiels::STATUT_DETENUE)
     AND parcelles.deleted_at IS NULL
```

Au moins une parcelle Détenue → terrain acté. Aucun dossier rattaché
→ terrain non acté.

Lecture en SQL brut (`EXISTS`), **sans** les scopes ACL dossier
(`RecordHabilitationScope`) ni visibilité parcelle : l’étape est un
fait d’opération, visible de qui voit le programme. Aucune écriture
Foncier.

## DROITS RETENUS

Permission `programmes.ecriture` (« Créer et modifier les programmes »).

Contrôle serveur : `abort_unless` + `PermissionService::userHasPermission`
(groupes via `m365GroupNames()` brut — pas `hasAnyM365Group`, pas
`Gate::allows` : `Gate::before` short-circuit GS_ADMIN_ERP). Middleware
route `feature.permission:programmes.ecriture` en plus.

Catalogue `config/feature_permissions.php` (brut) :

| Groupe | `programmes.ecriture` |
|---|---|
| GS_DIRECTION_GENERALE | oui (liste explicite) |
| GS_ADMIN_ERP | oui (`*`) |
| GS_DSI | oui (`*`) |
| GS_ENVOL | non (a `commercialisation.ecriture` = grille de lots, autre droit) |
| GS_COMPTABILITE | non (`programmes.lecture` seulement) |
| GS_HECTARE | non |

Le compte Robin (GS_ADMIN_ERP) est faussement positif pour un test de
refus. Tester le POST 403 avec un compte sans `programmes.ecriture`.

## MAPPING FILTRE LISTE

| Libellé affiché | Valeur postée | Libellé back |
|---|---|---|
| Non commercialisé | `non_commercialise` | Non commercialisé |
| Pré-commercialisation | `pre_commercialisation` | Pré-commercialisation |
| Commercialisation | `commercialisation` | Commercialisation |

Défaut UI : Tous (`tous`) = pas de clause SQL.

## ARBITRAGES ROBIN (non touchés)

- Onglets toujours visibles (AppShell inchangé).
- Chronologie / jalons / % avancement inchangés.
- Taux de commercialisation lots vendus inchangé.
- Pas de Teams / alerte / tâche.
- Pas de tranches / montage.
- Pas d’écriture Foncier.

## SYNTHÈSE

Interrupteur `commercialisation_active_at` / `_par_id` sur `programmes`.
Étape calculée SQL (liste) et PHP (fiche). Confirmation à l’activation
et à la désactivation. Audit via `AuditsFieldChanges` (ces deux champs
seulement).

## 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’étape commerciale n’est pas une saisie : un seul acte de gestion
(interrupteur) croisé avec un fait foncier lu en SQL. Ne pas passer
par `dossierFoncier()` Eloquent pour ce fait — le scope ACL dossier
ferait croire « terrain non acté » à qui voit le programme sans voir
le dossier. `commercialisation.ecriture` ≠ `programmes.ecriture`.
