---
suivi: 1173
date: 2026-08-04
sujet: Commercial — verrou AF CommercialController + gardes inline + anti-silence
chantier: commercialisation
type: fix
statut: poussé
hash: 662cc339
fichiers:
  - app/Http/Controllers/CommercialController.php
  - app/Http/Controllers/ReservationController.php
  - app/Http/Controllers/Concerns/ManagesCommercialLotPayload.php
  - app/Http/Requests/UpdateAcquereurFicheRequest.php
  - app/Services/ReservationAcquereurUpdateService.php
  - app/Models/Reservation.php
  - tools/diag/diag_commercial_incoherences_1171.php
  - tests/Unit/CommercialGardes1173Test.php
  - docs/suivi/SUIVI_1173_commercial_verrou_af_gardes.md
---

## PROMPT ENVOYÉ

SUIVI #1173 — PHP seul. Brancher verrou AF sur CommercialController ;
interdire statut reserve/acte + prix_vente à la création inline ; anti-silence
validateReservationUpdate ; Rule::in source_decouverte ; corriger faux positifs
diag #1171 (I6/I7/A1/sru). Aucune migration, aucune écriture données, aucun .vue.

## SYNTHÈSE

### Fiches lues
- `SUIVI_1171` — classes H/G/F ; LEÇON double chemin écriture.
- `SUIVI_1162` / `SUIVI_1168` — verrou AF factorisé (ne pas réécrire).
- `SUIVI_1156` — anti-silence withValidator.
- `SUIVI_1163` — soft-delete résa conserve documents (I6b).

### Objet 1 — CommercialController
- `action=acte` : `assertPrixVenteModifiable` puis `appliquerPrixLotPivot` si prix
  posté (même service #1162/#1168) ; message chiffré unifié (nb + montant).
- `action=reserve` : création (pas d’AF sur nouvelle résa) + `ensurePrincipalFromFk`.
- `update` grille : `prix_vente_ttc` déjà hors `LOT_GRILLE_WHITELIST` (#856).

### Chemins d’écriture `prix_vente_ttc` (exhaustif)

| Chemin | Protégé AF |
|--------|------------|
| Fiche acquéreur (`ReservationAcquereurUpdateService::update`) | OUI |
| `ReservationController::update` | OUI |
| `ReservationController::updatePrixLot` / rattacher | OUI (`assertPrixLotPivotModifiable`) |
| `ReservationController::store` (création) | N/A (pas d’AF) |
| `CommercialController` action=`acte` | OUI (#1173 via même service) |
| `CommercialController` action=`reserve` | N/A création |
| `CommercialController` update grille | N/A (prix interdit #856) |
| `ManagesCommercialLotPayload` create inline | N/A (prix **prohibited** #1173) |
| `ReservationLotsSync` (copie / Σ dérivée) | N/A sync interne |
| `ImportPegaoController` | NON — import initial lots/résas (hors parcours AF) |
| `ProgrammeEtapeObserver` → `appels_fonds` | N/A lit le prix, n’écrit pas `prix_vente_ttc` |
| Commandes artisan | aucune trouvée qui écrit `prix_vente_ttc` |

ImportPegao reste le seul chemin utilisateur hors verrou AF (création bulk, pas
modification post-AF). Signalé, non masqué.

### Objet 2 — inline create
- `statut` ∈ {en_stock, en_vente} seulement.
- `prix_vente_ttc` → `prohibited`.
- Aucune correction base (I2 = 0).

### Objet 3 — anti-silence
`validateReservationUpdate` : `Validator::after` comme #1156.
Champs réellement whitelistés = clés des rules (API JSON legacy ; **aucun**
appelant Vue actuel — la fiche passe par `UpdateAcquereurFicheRequest`).
Liste : prix_vente_ttc, remises, acte_en_main, frais_acte, agence_*, dates
(résa/acte/option/SSP/SRU/notaire/prêt/revente…), notaire*, acompte*,
paiement_comptant, commentaires, source_decouverte, notoriete_hectare,
banque_*, courtier_id, odf_recu, sru_statut, responsable_suivi_id, frais_inclus,
memos, condition_revente*, edd_*.

### Objet 4 — source_decouverte

| Libellé affiché | Valeur postée | Valeur stockée |
|-----------------|---------------|----------------|
| Panneau | panneau | panneau |
| Site web | site_web | site_web |
| Agence | agence | agence |
| Bouche à oreille | bouche_oreille | bouche_oreille |
| Réseaux sociaux | reseaux_sociaux | reseaux_sociaux |
| Publicité | publicite | publicite |
| Salon immobilier | salon_immobilier | salon_immobilier |
| Autre | autre | autre |
| (import Pegao, hors select) | — | pegao |

Const `Reservation::SOURCES_DECOUVERTE` + Rule::in store/update/fiche.

### Objet 5 — diag #1171
- I7 : AF vs `prix × pct_appel / 100`.
- I6 : orphelines vraies vs I6b soft-deleted (neutre #1163).
- A1b : 7 champs récemment livrés hors alerte.
- sru : plus de `= 0` sur VARCHAR ; répartition GROUP BY.

### Migration / données
Aucune. Aucune donnée modifiée.

## DÉPLOIEMENT-TEST

1. `git pull` ; `optimize:clear` ; `view:clear` (pas de journal si non ajouté)
2. `php tools/diag/diag_commercial_incoherences_1171.php` — I7=0, I6b libellé neutre, sru réel
3. `php tools/diag/diag_remises_kpi_1168.php` → 107 304,48 €
4. `php artisan commercial:coherence-reservation-lots` → 0
5. Smoke tests 1–10 du prompt (Commercial/Index acte + AF)

## LEÇON

MySQL `'en_attente' = 0` est TRUE : un contrôle de remplissage qui compare
toute colonne à 0 invente des 0 % sur les enums string. Verrou AF : un seul
service, tous les chemins — le troisième chemin oublié est pire qu’un 500.
