---
suivi: 1490
date: 2026-09-08
sujet: Schéma dépôt PROJET — poste_budgetaire_id + réactivation garde-fou #1448
chantier: facturation dossiers fonciers / destination PROJET
type: schema
statut: poussé
hash: ece48cc2
fichiers:
  - database/migrations/2026_09_08_191500_add_poste_budgetaire_id_to_depot_factures.php
  - app/Models/DepotFacture.php
  - app/Services/Foncier/DossierFoncierPosteBudgetaireService.php
  - tests/Unit/DepotProjetPosteBudgetaire1490Test.php
  - tests/Unit/DepotProjetPosteBudgetaire1490SourceTest.php
  - docs/suivi/SUIVI_1490_schema_depot_projet_poste_budgetaire.md
---

# SUIVI #1490 — Étape 1/4 : schéma dépôt + garde-fou #1448

## PROMPT ENVOYÉ

Étape 1 sur 4 du chantier factures destination PROJET : colonne
`depot_factures.poste_budgetaire_id` + réactivation du garde-fou #1448.
Pas d’UI, pas de Rule::in, pas de CLASSID, pas de liste factures dossier.
Rattachement PAR POSTE BUDGÉTAIRE (le poste porte le dossier). Libellé
destination : PROJET (singulier). Absence de CLASSID ne bloque pas le dépôt.

## BASE GIT

HEAD attendu `0b0fa1bf` (fiche #1488). #1489 et #1491 déjà sur main : ce lot
ne touche ni DepotFactureController / FactureFriseEtapePresenter / Show.vue
(#1489), ni le diagnostic timezone (#1491).

## FICHES LUES

- **#1467** — `poste_budgetaire_id` / `dossier_foncier_id` absents sur
  `depot_factures` et `factures`. Garde-fou `hasColumn` toujours false.
  Compte 627000 / CLASSID = étape 3. Fiche #1443 introuvable : ne pas la
  chercher.
- **#1448 LEÇON** — pas de colonne STORED : deux UNIQUE
  (`programme_id, poste_type_id` historique + `pb_dossier_type_uq`) et CHECK
  `pb_parent_xor`. Garde-fou écrit pour des colonnes *futures*.
- **#1450 LEÇON** — valideur = JSON `ocr_raw.saisie.validateur_id`, pas une
  colonne. Rule::in refuse encore PROJETS : **inchangé** (étape 2).
- **#1445** — CLASSID stocké sur `dossiers_fonciers.intacct_class_analytics_id`,
  non consommé à la comptabilisation ici (étape 3).

## CLAUSE D’ARRÊT — NON DÉCLENCHÉE

`postes_budgetaires.dossier_foncier_id` (nullable, XOR `programme_id`) +
relation `PosteBudgetaire::dossierFoncier()`. Chemin poste → dossier
non ambigu. Pas de colonne de contournement.

## A. LECTURE

### A1. Colonnes absentes vs ce qui existe

Ni `poste_budgetaire_id` ni `dossier_foncier_id` sur `depot_factures` /
`factures` avant ce lot (grep migrations hors 1490).

À la place sur `depot_factures` :
- `poste_budgetaire_type_id` — `2026_05_18_140000` L16 (nullable, après
  `fournisseur_id`) ; FK vers `postes_budgetaires_types`.
- `programme_budget_id` — `2026_07_28_210000` L17 (nullable, après
  `poste_budgetaire_type_id`).

Sur `factures` : `programme_id` / `marche_id` —
`2026_04_22_100100_create_factures_table.php` L13–14. Pas de poste ligne.

### A2. Garde-fou #1448 inactif avant ce lot

`DossierFoncierPosteBudgetaireService::posteUtiliseParFacture` L251–273 :
1. si `hasColumn('depot_factures', 'poste_budgetaire_id')` → `where` sur
   cette colonne ;
2. sinon si `dossier_foncier_id` + `poste_budgetaire_type_id` sur le dépôt ;
3. sinon `return false`.

Avant #1490 les deux `hasColumn` étaient faux → toujours false.
Chemin d’appel déjà câblé : `DossierFoncierPosteBudgetaireController::destroy`
L62–70 → `retirer()` L115–123 → `posteUtiliseParFacture()`. Payload UI
existant : `serializePoste` L218 `utilise_par_facture`.

### A3. `postes_budgetaires` — pas de STORED

`2026_04_18_100200` : UNIQUE `(programme_id, poste_type_id)`, `programme_id`
NOT NULL à la création.
`2026_09_04_144800` : `dossier_foncier_id` nullable L50, UNIQUE
`pb_dossier_type_uq` L55, CHECK `pb_parent_xor` L73–76, `MODIFY programme_id
… NULL` L63. **Aucune** colonne `GENERATED … STORED`. PROJECT_v72 était faux.

### A4. Poste → dossier

Colonne littérale : `postes_budgetaires.dossier_foncier_id`, **nullable**,
XOR avec `programme_id`. Relation `dossierFoncier()`.

### A5. Destinations dans le code (étape 2 / hors lot)

Rule::in actuel : `PROGRAMMES`, `FRAIS_GENERAUX`
(`DepotFactureController` L870, L995). UI bouton « PROJETS » désactivé
(`DepotFactureSaisieForm.vue` L8456). Variantes SQL tableau de bord :
`PaiementFondsPropres::DESTINATIONS_ELIGIBLES` L20–24 =
`PROGRAMMES`, `PROJET`, `PROJETS`. Ne pas modifier.

## B. MIGRATION

Colonne **uniquement** sur `depot_factures` : `poste_budgetaire_id` BIGINT
UNSIGNED NULL, `after('poste_budgetaire_type_id')` (Schema::table, #1421),
FK nommée `df_poste_budgetaire_fk`, **RESTRICT** on delete.

Pas de colonne sur `factures` : aucun chemin d’écriture sans
`DepotFactureController` (interdit #1489) ni `ComptabilisationController`
(étape 3). Le garde-fou ne lit que le dépôt. Double ancrage type
`marche_id` = plus tard.

Pas de `depot_factures.dossier_foncier_id` (décision Robin : le poste porte
le dossier).

`down()` : `dropForeign('df_poste_budgetaire_fk')` puis `dropColumn`.
Pas de `dropUnique` / `dropIndex` dans `up()`. Pas de `DB::transaction`.

NULL : `where('poste_budgetaire_id', $poste->id)` ne matche pas NULL
(SQL). Dépôts existants inchangés ; `retirer()` reste autorisé.

## C. RÉACTIVATION

Aucun changement de logique du garde-fou : `hasColumn` devient vrai.
Commentaire #1490 L256. Branche 2 (`dossier_foncier_id` sur le dépôt)
volontairement morte.

Chemin d’appel complet :
onglet Facturation → DELETE
`foncier.dossiers.postes-budgetaires.destroy` →
`DossierFoncierPosteBudgetaireController::destroy` →
`DossierFoncierPosteBudgetaireService::retirer` →
`posteUtiliseParFacture` →
`DepotFacture::where('poste_budgetaire_id', $poste->id)->exists()`.

Après migrate, tous les dépôts ont NULL : aucun blocage visible.
Preuve d’exécution = test (c) (ValidationException + requête SQL citant
la colonne). En prod, dès qu’un dépôt étape 2 sera rattaché.

## HORS PÉRIMÈTRE

Étapes 2–4, 627000, CLASSID, workflow Sabrina/Rudy, UI, Rule::in #1450,
fichiers #1485/#1487/#1489. Cast `ocr_raw => array` inchangé. Pas de
journal (aucun changement visible).

## DÉPLOIEMENT-TEST

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

Pas de `journal:sync`. Pas de rebuild Vite.

## LEÇON

Un `Schema::hasColumn` autour d’une colonne jamais migrée = garde-fou
mort. Réactiver = créer la colonne, pas réécrire le filtre. Ne pas
ajouter `factures.poste_budgetaire_id` « au cas où » : sans écriture
à la comptabilisation c’est un second ancrage orphelin. Rattachement
PROJET = ligne `postes_budgetaires`, pas FK dossier directe sur le dépôt.
