---
suivi: 1594
date: 2026-09-17
sujet: Fiche promesse — largeur, prix valorisé, ventilation
chantier: foncier / promesses
type: feature
statut: poussé
hash: b47cdb51
fichiers:
  - app/Models/PromesseFonciere.php
  - app/Models/PromesseComposantePrix.php
  - app/Models/TypeComposantePrix.php
  - app/Services/Foncier/PromesseFonciereService.php
  - app/Http/Controllers/Foncier/PromesseFonciereController.php
  - routes/web.php
  - database/migrations/2026_09_17_150000_create_promesse_composantes_prix_table.php
  - resources/js/Pages/Foncier/Promesses/Show.vue
  - tests/Unit/Promesse1594AffichageVentilationSourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1594_promesse_affichage_et_ventilation.md
---

# SUIVI #1594 — Fiche promesse : largeur, prix valorisé, ventilation

## PROMPT ENVOYÉ

Trois défauts prod sur la fiche promesse : affichage trop étroit
(chevauchements, CS illisible) ; « prix valorisé » incompréhensible ;
triptyque numéraire / valorisé / déclaré plus pauvre que la
ventilation des négociations. Diagnostic obligatoire du prix
valorisé avant tout retrait UI. Reprendre le mécanisme des
négociations. Élargir comme #1584. Ne pas toucher CS/avenants
logique (#1595), ni colonne `prix_valorise`, ni prix déclaré.

## FICHES LUES

- **#1089 LEÇON** — triptyque distinct, aucun total automatique,
  R9 montants absents du JSON sans `foncier.prix`. Impact : le
  prix déclaré reste un champ simple ; la ventilation ne le
  remplace pas. Le numéraire devient total calculé s’il y a des
  composantes.
- **#1118 LEÇON** — avenants empilés, `prixNumeraireEffectif` /
  `prixValoriseEffectif` overlays scalaires. Impact : **signaler**
  plutôt que refondre. On masque `nouveau_prix_valorise` à
  l’affichage ; `nouveau_prix_numeraire` reste un overlay scalaire
  (pas de re-ventilation d’avenant dans ce lot).
- **#1126 / #1136 / #1195 / #1252 LEÇON** — ventilation offres :
  table `offre_composantes_prix` (FK `offre_fonciere_id`, **pas**
  polymorphe), types `types_composantes_prix`, total = Σ, vide =
  montant saisissable. Impact : **pas mutualisable par table** ;
  on copie le mécanisme, on partage le référentiel de natures.
- **#1584 LEÇON** — contrainte = `max-w-*` sur le conteneur page,
  pas AppShell. Impact : ici `max-w-4xl` (56 rem) sur
  `Promesses/Show.vue`.
- **#1593 LEÇON** — même écran `Promesses/Show.vue`. Impact :
  sérialisé après #1593 (déjà sur `origin/main`). On ne touche
  pas la corbeille ni le verrou.

## DIAGNOSTIC — PRIX VALORISÉ

### Écriture

| Lieu | Fichier | Rôle |
|---|---|---|
| Colonne | `database/migrations/2026_07_30_180000_create_promesses_table.php:25` | `prix_valorise` decimal(14,2) nullable |
| Fillable | `app/Models/PromesseFonciere.php:37` | mass-assign |
| Formulaire (avant ce lot) | `PromesseFonciereController::validateInstruction` | était `nullable numeric` — **retiré du validate et du payload Show** |
| Avenants | `PromesseAvenantController.php:74` + `PromesseAvenantService.php:50-73` | `nouveau_prix_valorise` encore accepté si posté ; UI ne le poste plus |
| Import / Siwi | `tools/` grep `prix_valorise` | **aucun** (reprise #1225 ne touche pas les prix) |
| Seed / reprise | — | **aucune** écriture métier hors formulaire / avenant |

### Lecture

| Lieu | Usage réel ? |
|---|---|
| `PromesseFonciereController::serializePromesse` (avant) | affichage fiche uniquement |
| `DossierFoncierController.php:2715` | payload liste onglet Promesses — **la Vue n’affiche que `prix_numeraire`** (`Dossiers/Show.vue` `euro(pr.prix_numeraire)`) |
| `PromesseFonciere::prixValoriseEffectif()` `:258-260` | overlay avenant, **affichage seulement** |
| `BilanFinancierService` | **aucune** occurrence `promesse` / `prix_valorise` |
| Sage / faisabilité / supervision / export | **aucune** |
| Indicateur dossier | `DossierFoncierController.php:2567` : `prixNumeraireEffectif()` — **pas** le valorisé |
| Alertes expiration | `PromesseExpirationType.php:162` : `prixNumeraireEffectif()` |
| Acte (écart) | `ActeFoncierController.php:732` : `prixNumeraireEffectif()` |

### Calcul

**Aucun.** Jamais sommé, jamais converti, jamais envoyé à Sage.
R4 historique : « triptyque distinct — aucun total automatique ».

### Avenants « Nouveau prix valorisé »

`PromesseAvenantService` stockait un overlay scalaire lu par
`prixValoriseEffectif()`. Sans lecteur métier, l’overlay ne
servait qu’à l’UI. Ce lot **masque** le champ (formulaire +
historique). Colonne `promesse_avenants.nouveau_prix_valorise`
**conservée**. Pas de re-ventilation des avenants : un avenant
prix reste un `nouveau_prix_numeraire` scalaire (#1118). Une
re-ventilation d’avenant serait un lot distinct.

### Verdict

**AUCUN USAGE RÉEL.** Retrait de l’affichage fiche + avenants.
Colonne et données **intactes**. Aucune migration DROP.
`prixValoriseEffectif()` conservé pour lecture interne / tests
source avenants.

## CAUSE DE LA CONTRAINTE DE LARGEUR

Conteneur page `resources/js/Pages/Foncier/Promesses/Show.vue`
(avant) : `class="mx-auto max-w-4xl space-y-6 p-6"`.

`max-w-4xl` = 56 rem / 896 px. AppShell n’impose pas de max-width
sur le contenu principal. Même cause que #1584 (`max-w-7xl` sur
le dépôt de factures).

Corrigé : `w-full max-w-none space-y-6`. CS : grille
`lg:grid-cols-12` (type / butoir+état / actions), sans
`truncate`. Formulaire d’ajout CS : `md:grid-cols-2 xl:grid-cols-4`.

## VENTILATION — RÉUTILISATION

**Pas polymorphe.** `offre_composantes_prix.offre_fonciere_id`
pointe uniquement vers `offres_foncieres`. Extraire un service
partagé aurait touché `OffreFonciereController` / `Offres/Show.vue`
— **interdit** (on réutilise, on ne change pas).

Méthode :

- **Partagé** : référentiel `types_composantes_prix` /
  `TypeComposantePrix` (natures identiques).
- **Copié, non modifié** : UX Offres (ajout / retrait / total /
  vide = saisissable), sync delete+create, snapshot audit
  champ `ventilation`.
- **Table dédiée justifiée** : `promesse_composantes_prix`
  (`pr_comp_promesse_fk`, `pr_comp_type_fk`, `pr_comp_proprio_fk`).
  Migration DDL hors `DB::transaction`.

Total ventilé → `promesses.prix_numeraire`. Sans ligne, le
montant redevient saisissable. `mettreAJour` ignore
`prix_numeraire` posté si des composantes existent.

Prix déclaré : inchangé (champ simple, fiscal, hors ventilation).

Avenants : **signalé** — overlay scalaire conservé, pas de
ventilation d’avenant dans ce lot.

`TypeComposantePrix::estUtilise()` compte aussi les composantes
promesse (évite de désactiver un type encore utilisé).

## MAPPING NATURE DE COMPOSANTE

Sélecteur : `option-label="libelle"` / `option-value="id"`.
Valeur postée = `types_composantes_prix.id` (entier). Le `code`
sert au back (libellé libre si `autre`).

| Libellé affiché | Valeur postée | Code back | Libellé back |
|---|---|---|---|
| Numéraire | `type_composante_id` (id) | `numeraire` | Numéraire |
| Dation de lots | id | `dation_lots` | Dation de lots |
| ODF | id | `odf` | ODF |
| Échange de terrain | id | `echange_terrain` | Échange de terrain |
| Prise en charge VRD | id | `prise_en_charge_vrd` | Prise en charge VRD |
| Complément de prix conditionné | id | `complement_prix_conditionne` | Complément de prix conditionné |
| Rente viagère | id | `rente_viagere` | Rente viagère |
| Autre | id | `autre` | Autre (+ `libelle_libre` obligatoire) |

Seed : `2026_07_31_140100_create_types_composantes_prix_table.php`.

## SQL VOLUMÉTRIE (lecture seule)

```sql
SELECT
    COUNT(*) AS n_total,
    SUM(CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) AS n_vivantes,
    SUM(CASE WHEN deleted_at IS NULL AND prix_valorise IS NOT NULL THEN 1 ELSE 0 END) AS n_valorise_vivantes,
    SUM(CASE WHEN deleted_at IS NULL AND prix_valorise IS NOT NULL
        AND prix_numeraire IS NOT NULL AND prix_valorise <> prix_numeraire THEN 1 ELSE 0 END) AS n_different_numeraire
FROM promesses;

SELECT COUNT(*) AS n_avenants_valorise
FROM promesse_avenants
WHERE nouveau_prix_valorise IS NOT NULL;
```

Résultat local : MySQL `127.0.0.1:3306` refusé (aucune instance
en écoute). Dump `hectareenvol.sql` : schéma seulement, aucun
`INSERT INTO promesses`. Exécuter la requête **sur OVH** après
`migrate` ; coller les totaux. Les données `prix_valorise` ne
sont pas touchées par ce lot.

## SYNTHÈSE

Fiche pleine largeur. Prix valorisé retiré de l’UI (colonne
conservée). Ventilation du prix numéraire comme une négociation,
table dédiée, types partagés. Prix déclaré inchangé. Avenants :
affichage valorisé masqué, overlay numéraire scalaire conservé.

## DÉPLOIEMENT-TEST

OVH : `git pull origin main` puis `php artisan migrate --force`
(table `promesse_composantes_prix`) ; `php artisan optimize:clear` ;
`php artisan view:clear` ; `php artisan route:clear` ;
`php artisan journal:sync`.

Tests manuels : largeur ; CS lisibles ; ventilation ajout/retrait/total ;
sans ligne, montant saisissable ; prix déclaré inchangé ; promesse
existante sans perte ; SQL volumétrie inchangée après déploiement.

## LEÇON

`offre_composantes_prix` n’est pas polymorphe : une ventilation
promesse = **nouvelle table**, pas un morph. Extraire un service
partagé toucherait les négociations — interdit si la consigne est
« on réutilise, on ne change pas ».

`max-w-4xl` sur la fiche promesse (896 px) suffit à faire
chevaucher CS + triptyque ; AppShell n’est pas en cause.

`prix_valorise` n’alimente ni bilan ni indicateur : seul
`prixNumeraireEffectif()` est lu hors fiche. Un overlay avenant
sans lecteur = affichage mort. Ne jamais DROP la colonne pour
autant.
