---
suivi: 1154
date: 2026-08-03
sujet: Diagnostic — module commercial (cardinalités, KPI, prix, écrans)
chantier: commercialisation
type: diagnostic
statut: poussé
hash: 368495f7
fichiers:
  - tools/diag/diag_commercial_1154.php
  - docs/suivi/SUIVI_1154_diag_commercial.md
---

## PROMPT ENVOYÉ

SUIVI #1154 — PHASE 1 DIAGNOSTIC (NE PAS CODER métier). Avant découpage des 16
demandes commerciales (#67, #81–#88, #96–#102, #106), instruire l’état réel du
module : tables, cardinalités réservation↔lot (#85) et réservation↔acquéreur
(#101), écrans Vue, statuts/motifs, KPI remises/annulations, prix de vente et
couplage comptable, notaires, plan PDF, suppression lot, dates, filtres.
Script `tools/diag/` lecture seule, commit + push. Aucune écriture base /
migration / appel externe.

## SYNTHÈSE

### Fiches lues (consultation-suivi)

- `SUIVI_1062_diag_grille_prix_lotissement.md` — `lots` vestigial vs
  `lots_commerciaux` ; `prix_vente_ttc` figé à la réservation ; colonnes UI
  en dur dans `LotsGrillePanel.vue`.
- `SUIVI_1064_grille_lotissement_hectare.md` — pas de pagination ; total Σ
  prix sans filtre DataTable.
- `SUIVI_1066_saisie_lots_en_ligne.md` — saisie inline ; `prix_vente_ttc`
  optionnel à la création.
- `SUIVI_1121_diag_categorie_fournisseurs.md` — **aucune** catégorie
  fournisseur ; selects notaire = tous actifs. Change l’approche #82.
- Pas de fiche `SUIVI_856` / `SUIVI_914` (références commentaires code
  seulement : whitelist grille, pipeline remise en stock).
- Dernier numéro avant ce lot : **#1153**.

---

### Q1 — Cartographie des tables

| Entité | Table réelle | Notes |
|--------|--------------|-------|
| Réservations | `reservations` | SoftDeletes ; modèle `Reservation` |
| Lots commerciaux | `lots_commerciaux` | **vrai objet métier** ; SoftDeletes |
| Lots vestigiaux | `lots` | Coquille (~id / prestation / timestamps) — **ne pas utiliser** |
| Acquéreurs | `contacts` | + pivot `contact_programme` (annuaire) |
| Co-acquéreurs | **pas de table** | JSON `contacts.co_acquereur` + FK optionnelle `co_contact_id` |
| Motifs annulation | **pas de table** | Colonne `reservations.motif_annulation` (string) + const PHP |
| Notaires | **pas de table** | `fournisseurs` via FK sur réservation (+ texte legacy) |

Enfants lot : `lot_emplacements`, `lot_annexes`, `lot_commercial_historique`,
`offres_commerciales`, `appels_de_fonds`, `tmas` (nullable).

**Coller sortie script Q1** (colonnes complètes + volumétrie prod).

---

### Q2 — Cardinalité réservation ↔ lot (décide #85)

**Verdict : 1 lot par réservation** via FK directe
`reservations.lot_commercial_id` → `lots_commerciaux.id`.

- **Pas de pivot** multi-lots.
- Côté lot : `HasMany` réservations (historique d’annulations possible).
- Garde métier : au plus **une** réservation `en_cours` par lot
  (`ReservationController::store`).
- FK migration : `cascadeOnDelete` ; **pas d’UNIQUE** sur `lot_commercial_id`.

Passage à N lots (#85 garage flottant) = **nouveau modèle** (pivot ou
table de liaison) + réécriture de tout le périmètre ci-dessous.

**Périmètre d’impact N** : `Reservation` (+ relations/fillable),
`ReservationController`, `CommercialController`, `CommercialisationController`,
`LotCommercialController`, `AcquereurController`,
`ReservationAcquereurUpdateService`, `GenerationAppelFondService`, PDF AF,
`TMAController`, `AppelsDeFondsController`, `OffreCommercialeController`,
`AlerteService`, `SharePointService`, `WidgetDataService`,
`ImportPegaoController`, **`LotsGrillePanel.vue` (~3399 L)**,
`Acquereur/Show.vue`, KPI remises PHP+Vue, panneaux TMA.

**Coller Q2 prod** (distribution lots ayant N réservations ; >1 `en_cours`).

---

### Q3 — Cardinalité réservation ↔ acquéreur (décide #101)

**Verdict : 1 acquéreur principal + au plus 1 co-acquéreur.**

| Rôle | Stockage |
|------|----------|
| Principal | `reservations.contact_id` → `contacts` |
| Co-acquéreur | `contacts.co_acquereur` (JSON civilité/nom/…) **et/ou** `contacts.co_contact_id` |
| Situation maritale | **`contacts.situation_maritale`** (1 champ / contact principal) |
| Régime matrimonial | **`contacts.regime_matrimonial`** (idem — **pas** individualisé co-acquéreur) |

Pas de pivot `reservation_contacts`. UI : toggle `co_acquereur_actif` dans
modale réservation + fiche acquéreur.

#101 (N co-acquéreurs + situation/régime **par personne**) exige une table
de liaison personnes ↔ réservation (ou N contacts liés) — **avant** d’ajouter
des champs matrimoniaux isolés.

**Coller Q3 prod** (nb contacts/résas avec co-acquéreur).

---

### Q4 — Écrans Vue et volumétrie (sérialisation)

| Écran | Fichier | Lignes |
|-------|---------|--------|
| (a) Page commercialisation | `resources/js/Pages/Programmes/Commercialisation.vue` | **175** |
| (a) Grille (cœur) | `resources/js/Components/Commercialisation/LotsGrillePanel.vue` | **3399** |
| (b) Formulaire réservation | **modale dans** `LotsGrillePanel.vue` | — |
| (c) Fiche acquéreur | `resources/js/Pages/Programmes/Acquereur/Show.vue` | **1093** |

Composants voisins : `StockSynthesePanel.vue` (120), `SynthesePanel.vue` (278),
`ProspectsPanel.vue` (492), `CommercialDocumentsPanel.vue` (28), legacy
`Pages/Commercial/Index.vue` (373).

**Zone commune critique** : `LotsGrillePanel.vue` porte grille + réservation +
KPI client + annulation + plan. **Interdit** de paralléliser deux lots qui
touchent ce fichier (`manifest.json` + écrasement UI).

---

### Q5 — Statuts et motifs (décide #84, #87)

| Liste | Valeurs exactes code/base | Source |
|-------|---------------------------|--------|
| Statut lot | `en_stock`, `en_vente`, `reserve`, `acte`, `annule` | ENUM MySQL + const `LotCommercial` + libellés UI |
| Statut réservation | `en_cours`, `acte`, `annule` | ENUM + const `Reservation` |
| SRU | `en_attente`, `en_cours`, `purge`, `retracte` | const PHP |
| Motifs annulation | `financement_refuse`, `financement_hors_delai`, `retractation_sru`, `desistement_client`, `situation_personnelle`, `prix_eleve`, `autre_bien`, `non_paiement_depot`, `modification_programme`, `retard_chantier`, `abandon_programme`, `modification_lot`, `probleme_juridique`, `accord_amiable`, `echange_lot`, **`erreur_saisie`**, `autre` | **Const PHP** `MOTIFS_ANNULATION_VALIDES` + miroir Vue — colonne **string**, pas table |
| Type financement | ENUM contacts : `comptant\|pret_bancaire\|ptz\|mixte\|autre` ; validation étend `investissement`, `pret` | ENUM DB + `Rule::in` |

Ajout de valeurs (#84 motifs, #87 financement) = modifier const PHP **et**
options Vue **et** éventuellement ALTER ENUM contacts (financement).

**Coller Q5 prod** (distributions réelles).

---

### Q6 — KPI annulations et remises (décide #83, #102)

**Remises accordées** — calcul **PHP** :
`CommercialisationController::calculerRemisesAccordees` :

```
Σ max(0, prix_grille_ttc − prix_vente_ttc)
pour lots statut ∈ {reserve, acte}
où prix_vente_ttc < prix_grille_ttc
```

Duplicata **Vue** : `LotsGrillePanel.vue` `kpis.remises_accordees` (même
formule). Bandeau `remises_promotions` = remises + promotions actives
(`offres_commerciales`).

**Annulations** : **aucun KPI dédié** (pas de taux/filtre motif dans le
bandeau). `erreur_saisie` existe dans la liste de motifs mais **n’est filtré
nulle part** dans les KPI.

#83 → point d’injection : `calculerRemisesAccordees` **et** le computed Vue
(et tout futur KPI annulation). Aujourd’hui une annulation remet le lot hors
`reserve`/`acte` → la remise sort du KPI *par statut*, pas par motif.

#102 (ajouter frais d’acte à la remise) : la formule actuelle = **écart
grille−vente uniquement**. `frais_inclus` (JSON Notaire/Procuration/…) et
`remises_commerciales_ttc` (champ réservation) **ne rentrent pas** dans ce
KPI. Il faudra décider si on enrichit la formule serveur+client ou un autre
indicateur.

**Coller Q6 prod** (montant agrégé + lots avec hist. `erreur_saisie`).

---

### Q7 — Prix de vente (décide #86) — SENSIBLE

| Question | Réponse |
|----------|---------|
| Audité `entity_field_audits` ? | **Non** (foncier uniquement) |
| Historique commercial ? | `reservation_historique` (fiche acquéreur) + `lot_commercial_historique` (grille) |
| Modifiable via grille ? | **Non** — `LOT_GRILLE_WHITELIST` exclut `prix_vente_ttc` (#856) |
| Modifiable fiche résa en cours ? | Oui via `UpdateAcquereurFicheRequest`, **sauf** si appels de fonds générés (`prixVenteChangeBlocked`) |
| Verrou post-acte ? | Pas de hard-lock DB ; confirmation UX si champs sensibles ; AF = vrai verrou |

**Lecteurs de `prix_vente_ttc`** :
- Sync lot ← réservation (`ManagesCommercialLotPayload` / `CommercialController`)
- **Appels de fonds** : `GenerationAppelFondService` —
  `montant = prix_vente_ttc × (% stade)` → PDF + **sync Intacct**
- `BilanFinancierService` (valorisation lot)
- KPI remises / grille Vue
- `ProgrammeEtapeObserver` (AF auto)
- Fiche acquéreur

`lignes_financement` : **non lié** au prix réservation.

**Verdict #86** : rendre le prix modifiable « en cours » est déjà partiel ;
l’élargir sans garde-fous **recalcule la base des AF / Intacct**. Nature du
lot = **changement comptable**, pas simple UX.

**Coller Q7 prod** (volumes historiques / AF).

---

### Q8 — Notaires (décide #82)

| Niveau | Stockage |
|--------|----------|
| Programme | **Aucun** champ notaire |
| Réservation | Texte legacy `notaire` / `notaire_double_minute` + FK `notaire_fournisseur_id` / `notaire_double_minute_fournisseur_id` |

Alimentation sélecteur : **tous** les fournisseurs actifs
(`FournisseurSelectionOptions::forCommercialSelect`) — pas de référentiel
notaire (confirmé #1121).

**Cause probable bug « une fois sur deux »** :
1. Fiche acquéreur : whitelist = **FK seulement** ; champs texte `notaire*`
   absents de `UpdateAcquereurFicheRequest` → **ignorés silencieusement**.
2. Select parmi des milliers sans filtre métier → mauvaise sélection /
   valeur perdue côté PrimeVue filter.
3. Affichage lecture : fallback `notaire_fournisseur?.nom ?? notaire` masque
   l’incohérence FK vide / texte rempli.

**Coller Q8 prod** (FK vs texte, volume fournisseurs).

---

### Q9 — Plan PDF (décide #67)

- Colonne : `lots_commerciaux.plan_fichier_path`
- Stockage : disk `local` `lots_commerciaux/{id}/plans` (pas SharePoint plan,
  pas `entity_documents`)
- Backend upload **existe** : `LotCommercialController::uploadPlan` + route
  `POST …/lots-commerciaux/{lot}/plan`
- UI : viewer si path présent ; **aucun bouton d’upload** dans
  `LotsGrillePanel` (grep POST `/plan` = 0 côté Vue)

Mélanie « plus de bouton » : le bouton n’est **pas** conditionné par un
droit/statut dans le composant actuel — il est **absent**. Cause = UI non
branchée (régression ou jamais livrée côté grille), pas un ACL.

**Coller Q9 prod** (nb lots avec plan).

---

### Q10 — Suppression lot (décide #99)

`LotCommercial` SoftDeletes. `destroy` autorisé si `statut === en_stock`.
Soft-delete **ne cascade pas** en MySQL.

| Enfant | ON DELETE (migration) |
|--------|------------------------|
| `reservations` | cascade (hard delete only) |
| `offres_commerciales` | cascade |
| `appels_de_fonds` | cascade |
| `lot_commercial_historique` | cascade |
| `lot_emplacements` / `lot_annexes` | cascade |
| `tmas` | **nullOnDelete** |

**Données rattachées à lister pour #99** : réservations (toute statut),
AF, offres promo, TMA, historiques, emplacements, annexes, fichier plan,
dossiers SharePoint via résa. Purge hard : `CommercialController::purge`
(GS_DSI).

**Coller Q10 prod** (FK info_schema + volumes + candidats en_stock vides).

---

### Q11 — Dates de suivi (décide #96, #97)

Champs date déjà présents (échantillon métier) :
`date_notif_sru`, `date_ar_client`, `date_expir_reflexion`,
`date_sru_envoi`, `date_sru_fin_delai`, `date_prev_signature_acte`,
`date_acte`, `date_reelle_signature_acte`, `date_rdv_signature`,
`date_envoi_notaire`, + financement / acomptes / revente / `date_annulation`.

**Auto J+n** : `ReservationObserver::saving` — si `date_sru_envoi` dirty →
`date_sru_fin_delai = +10 jours`, `sru_statut = en_cours`. Pas d’auto sur
`date_expir_reflexion`.

**Verrou `date_prev_signature_acte`** : **pas** de readonly code après
création ; DatePicker éditable sur fiche (même post-acte, avec confirmation
si sensible). `date_sru_fin_delai` affichée calculée / dérivée.

Note : `date_envoi_notaire` **absent** de la whitelist
`UpdateAcquereurFicheRequest` → risque d’ignore silencieux si posté depuis
la fiche.

**Coller Q11 prod** (taux de remplissage par colonne).

---

### Q12 — Recherche et filtres (décide #100, #106)

- Chargement : **tout le programme** en une requête (`->get()`), pas de
  pagination serveur.
- Affichage : tri client `numero_lot` ; **pas** de filtre / recherche
  DataTable sur la grille actuelle (#1064).
- Legacy `Commercial/Index.vue` : filtre statut client-side seulement.

**Coller Q12 prod** (total lots + top programme).

---

## DÉCOUPAGE RECOMMANDÉ

Ordre imposé par les cardinalités et `LotsGrillePanel.vue` (zone série).

### Lot A — Socle cardinalité lots (#85) — PREMIER
Pivot / liaison réservation↔N lots. Touche modèle, migrations, contrôleurs
AF/KPI, **`LotsGrillePanel`**, fiche acquéreur. **Bloque** tout ajout de
champs « un lot » sur la réservation.

### Lot B — Socle cardinalité co-acquéreurs (#101) — DEUXIÈME
Table personnes liées + situation/régime **par personne**. Touche
`contacts`, `Acquereur/Show.vue`, modale dans **`LotsGrillePanel`** →
**série après A** (même arbre Vue).

### Ensuite (dépendent de A et/ou B si champs sur résa/personnes)

| Lot proposé | Tickets | Fichiers dominants | Dépend |
|-------------|---------|--------------------|--------|
| C — Motifs / financement référentiels | #84, #87 | `Reservation.php`, Vue options, éventuellement ENUM contacts | faible ; série si touchent Grille |
| D — KPI remises / annulations | #83, #102 | `CommercialisationController`, `LotsGrillePanel`, éventuellement `SynthesePanel` | formule après A (N lots) |
| E — Prix vente réservation | #86 | `ReservationAcquereurUpdateService`, `UpdateAcquereurFicheRequest`, `Acquereur/Show`, garde AF | **après** clarification comptable ; série vs D sur montants |
| F — Notaires UX | #82 | `Acquereur/Show`, options fournisseur (#1121 rôles), whitelist | indépendant schéma A/B |
| G — Plan PDF upload UI | #67 | `LotsGrillePanel` + route existante | **série** Grille |
| H — Suppression lot gardes | #99 | `LotCommercialController`, règles enfants | après A (définition « rattaché ») |
| I — Dates suivi SRU/acte | #96, #97 | `Reservation`, Observer, `Acquereur/Show`, whitelist dates | léger ; attention ignore silencieux |
| J — Recherche / filtres grille | #100, #106 | `LotsGrillePanel`, éventuellement pagination serveur `CommercialisationController` | **série** Grille ; volumétrie Q12 |

Tickets **#81, #88** (hors détail dans le prompt) : rattacher au lot dont
les fichiers réels coïncident après lecture Aktor — ne pas les fusionner
avec A/B par défaut.

**Règle d’or** : un seul lot à la fois sur
`LotsGrillePanel.vue` + `public/build/`. Rebase `origin/main` avant chaque
build.

---

## CE QUI EST PLUS RISQUÉ QU’IL N’Y PARAÎT

1. **#86 prix de vente** — alimente **appels de fonds** et donc la chaîne
   **Intacct**. Ce n’est pas un champ formulaires : toute modification après
   génération AF casse la cohérence montants / PDF / sync. Verrou actuel =
   présence d’AF non annulés.

2. **#85 multi-lots** — presque tout le module assume `lotCommercial`
   singulier (AF uniques par lot+stade, SharePoint dossier, KPI par lot,
   sync montants). Coût >> une colonne FK.

3. **#101 multi co-acquéreurs** — situation/régime aujourd’hui sur le
   **contact principal uniquement** ; SharePoint lit déjà `co_acquereur`
   JSON. Individualiser sans table de liaison = dette immédiate.

4. **#83 erreur_saisie hors KPI** — il n’y a **pas** de KPI annulation
   aujourd’hui ; le KPI remises ignore les motifs. Clarifier le besoin
   métier avant de « filtrer » un indicateur inexistant.

5. **#102 frais d’acte dans la remise** — la remise KPI ≠
   `remises_commerciales_ttc` ≠ lignes `frais_inclus`. Trois concepts
   homonymes ; les fusionner sans cadrage fausse le bandeau commercial.

6. **#82 notaire** — échec **silencieux** (hors whitelist) + select non
   filtré : corriger la persistance/feedback avant d’ajouter un
   référentiel (sinon le bug « disparaît » encore).

7. **#67 plan** — backend prêt, UI absente : risque de « re-cacher » le
   bouton derrière un droit inutile ; brancher l’upload existant.

8. **#99 suppression** — cascades MySQL seulement en **hard** delete ;
  soft-delete laisse des orphelins logiques (résas soft, AF). « Sans
  données rattachées » doit être une checklist applicative, pas un
  `DELETE` SQL.

---

## DÉPLOIEMENT / TEST

```text
git pull origin main
# Aucune migrate. Aucun npm.
php tools/diag/diag_commercial_1154.php
```

Coller la sortie complète dans cette fiche (sections Q1–Q12). Vérifier
surtout Q2 (cardinalité), Q3 (co-acquéreurs), Q6 (remises), Q7 (AF), Q12
(plus gros programme).

## LEÇON

Ne jamais ouvrir des lots « champs formulaires » sur le commercial tant que
les cardinalités #85 / #101 ne sont pas tranchées : chaque champ ajouté sur
`reservations` ou `contacts` comme s’il n’y avait qu’un lot / un
co-acquéreur sera réécrit. `LotsGrillePanel.vue` (~3400 L) est le goulot
de sérialisation front — un lot = un commit build.
