# AUDIT — Élimination de la dimension « prestation » sans perte de données

**Date :** 2026-05-20  
**Périmètre :** lecture seule du dépôt `erp-immo` (aucune modification de code).  
**Objectif cible :** passer de **rubrique → prestation → poste** à **rubrique → poste** (`postes_budgetaires_types`), puis retirer `prestations` et toutes les FK `prestation_id`.

**Contrainte absolue :** prod OVH (`https://envol.hectare.fr`) — aucune perte, aucune ligne orpheline, reprise des liens **avant** toute suppression.

---

## Synthèse exécutive

| Constats | Impact |
|----------|--------|
| La couche **métier courante** (création/édition marchés) utilise déjà `poste_budgetaire_type_id` ; les nouveaux marchés fixent `prestation_id = null` (`MarcheController`). | La migration applicative est **partiellement faite**. |
| **Legacy fort** : `marches.prestation_id`, jointures bilan, concessionnaires, TMA, SharePoint, API dépôt factures, et `posteBudgetaireTypePourMarche()` avec fallback via prestation. | Risque de **trous de données** si on droppe trop tôt. |
| `prestations.compte_comptable` = **comptes GL** (331453, 333000, …) issus du CSV référentiel. | **Ne pas confondre** avec `postes_budgetaires_types.compte_comptable`, souvent un **CLASSID Intacct** (analytique). |
| L’**APBILL Intacct** (`ACCOUNTNO`) vient de `comptes_comptables.accountno` via `lignes_comptables[].compte_id` — **pas** de `prestations` ni du poste bilan aujourd’hui. | Supprimer `prestations` ne casse pas l’envoi Intacct **tel qu’il est codé** ; en revanche le **référentiel GL par lot/prestation** n’est pas encore porté par le poste. |
| `PrestationsPostesSeeder` existe mais **n’est pas appelé** dans `DatabaseSeeder`. | Sur env frais, `prestations.poste_budgetaire_type_id` peut rester NULL. |
| Table `lots` (technique) : FK `prestation_id`, modèle quasi mort ; **lots commerciaux** = autre table. | Faible risque, à traiter avec la table `lots`. |

**Recommandation :** migration en **3 vagues** — (1) données prod + cartographie GL, (2) code & jointures, (3) schéma (drop FK puis table) — avec scripts SQL de contrôle **bloquants** entre chaque vague.

---

## 1. Base de données — références `prestation`

### 1.1 Table `prestations`

| Élément | Détail |
|---------|--------|
| Migration | `database/migrations/2026_04_19_100000_create_prestations_table.php` |
| Colonnes | `id`, `categorie` (enum 5 valeurs), `libelle`, `code_prestation` (unique), `description`, **`compte_comptable`** (string **NOT NULL**), `actif`, timestamps |
| FK entrantes | Aucune sur la table elle-même |
| FK sortante | `poste_budgetaire_type_id` → `postes_budgetaires_types` (nullable, `nullOnDelete`) — migration `2026_05_05_170500_add_poste_budgetaire_type_id_to_prestations_table.php` |

**Si on droppait `prestations` aujourd’hui :** échec MySQL sur les FK listées ci-dessous (sauf lignes déjà détachées).

### 1.2 FK `prestation_id` et dépendances

| Table | Colonne | Nullable | FK → `prestations` | `onDelete` | Migration d’origine | Effet d’un DROP `prestations` |
|-------|---------|----------|-------------------|------------|-------------------|-------------------------------|
| `marches` | `prestation_id` | oui | oui | **restrict** | `2026_04_20_100100_create_marches_table.php` | **BLOQUANT** tant qu’un marché référence une prestation |
| `lots` | `prestation_id` | oui | oui | **nullOnDelete** | `2026_04_19_100100_create_lots_table.php` | Les `lots.prestation_id` passeraient à NULL (table peu utilisée) |
| `prestations` | `poste_budgetaire_type_id` | oui | → `postes_budgetaires_types` | nullOnDelete | `2026_05_05_170500_...` | N/A (table cible) |

### 1.3 Colonnes liées sans FK vers `prestations`

| Table | Colonne | Notes |
|-------|---------|-------|
| `marches` | `poste_budgetaire_type_id` | FK → `postes_budgetaires_types`, nullable, `nullOnDelete` — migration `2026_05_19_140000_add_poste_budgetaire_type_id_to_marches_table.php` + **backfill SQL** depuis `prestations.poste_budgetaire_type_id` |
| `marches` | `poste_budgetaire_id` | FK → `postes_budgetaires` (instance programme), nullable — `2026_05_05_165000_...` |
| `depot_factures` | `poste_budgetaire_type_id` | FK directe — `2026_05_18_140000_add_direct_fk_columns_to_depot_factures.php` |

### 1.4 Migrations « pont » déjà en place

```sql
-- 2026_05_19_140000 : copie poste depuis prestation vers marché si poste direct absent
UPDATE marches m
JOIN prestations p ON p.id = m.prestation_id
SET m.poste_budgetaire_type_id = p.poste_budgetaire_type_id
WHERE m.poste_budgetaire_type_id IS NULL
  AND p.poste_budgetaire_type_id IS NOT NULL;
```

**Point prod documenté (`PROJECT.md`) :** backfill OVH a pu mettre **tous** les marchés sur `poste_budgetaire_type_id = 20` (C_TRAVAUX_VRD) — correction métier manuelle attendue, indépendamment de la suppression de `prestations`.

### 1.5 TMA — homonymie « prestation »

| Fichier | Contenu |
|---------|---------|
| `database/migrations/2026_05_13_160000_create_tmas_table.php` | Catégorie enum `prestations_sup` (prestations supplémentaires TMA) |
| `database/migrations/2026_05_13_200000_tma_prestation_workflow_and_avenant_tma.php` | Statut `prestation_realisee`, colonne `date_prestation_realisee` |

**Ce n’est pas** la table `prestations` : ne pas renommer/supprimer sans analyse UX séparée.

---

## 2. Modèles Eloquent

### 2.1 `App\Models\Prestation`

| Relations | Type |
|-----------|------|
| `lots()` | hasMany `Lot` |
| `marches()` | hasMany `Marche` |
| `posteBudgetaireType()` | belongsTo `PosteBudgetaireType` |

| Scopes | Comportement |
|--------|--------------|
| `categorie($cat)` | filtre `categorie` |
| `actif()` | `actif = true` |

Pas d’accesseur métier comptable ; pas de logique Intacct.

### 2.2 Modèles avec relation vers `Prestation`

| Modèle | Relation | Champ FK |
|--------|----------|----------|
| `Marche` | `prestation()` belongsTo | `prestation_id` |
| `Lot` | `prestation()` belongsTo | `prestation_id` |
| `PosteBudgetaireType` | `prestations()` hasMany | `poste_budgetaire_type_id` sur `prestations` |

### 2.3 Résolution poste sur marché (critique)

`MarcheController::posteBudgetaireTypePourMarche()` :

1. `marches.poste_budgetaire_type_id` (direct)  
2. sinon `prestation.poste_budgetaire_type_id` (legacy)

Tant que cette méthode existe, **supprimer la table `prestations` casse les marchés legacy** n’ayant que `prestation_id`.

---

## 3. Code applicatif (`app/`)

Légende : **L** = lecture seule, **E** = écriture, **M** = logique métier critique.

### 3.1 Contrôleurs

| Fichier | Lignes (indicatif) | Cat. | Rôle |
|---------|-------------------|------|------|
| `MarcheController.php` | 56-57, 636-637 | L | Eager-load `prestation.posteBudgetaireType` |
| | 193, 401, 568 | E | Force / tente `prestation_id = null` à la création / changement de poste |
| | 397-432 | M | Détachement legacy `prestation_id` ; fallback si contrainte DB |
| | 856, 1269-1274, 1649-1668, 1745-1748 | L/M | Payload API inclut encore `prestation` ; résolution poste via prestation |
| | 1818-1849 | L | Liste postes formulaire (nom variable `$prestations` = **postes types**) |
| `DepotFactureController.php` | 1876-1900 | L | Liste marchés : expose `prestation_id`, libellé `prestation` |
| | 411, 805, 2239 | E | `poste_budgetaire_type_id` en écriture (OK) |
| `ConcessionnairesController.php` | 19-28, 55, 65-67 | M | Filtre marchés via **`whereHas('prestation')`** + match libellé poste concessionnaire |
| `TMAController.php` | 37-45 | L | Charge `prestation_id` + libellé poste via `prestation.posteBudgetaireType` |
| `Admin/NomenclatureController.php` | 288-291, 353-356 | E | Suppression poste : delete `Prestation` orphelines ; usage = `prestation` avec marchés (**ne compte pas** `marches.poste_budgetaire_type_id` seul) |
| `ComptabilisationController.php` | — | — | **Aucune** référence à `prestation` (APBILL via `comptes_comptables`) |

### 3.2 Services

| Fichier | Lignes | Cat. | Rôle |
|---------|--------|------|------|
| `BilanFinancierService.php` | 544-552, 648-655, 676-679 | M | Jointures **`prestations`** pour agrégats marchés / factures ; `COALESCE(m.poste_budgetaire_type_id, p.poste_budgetaire_type_id)` partiel |
| | 647-655 | M | `resolvePosteBudgetaireIdForDepot` : si pas de poste sur dépôt, repasse par **marché → prestation** (inner join) |
| `SharePointService.php` | 1500-1509 | M | Nom dossier : fallback `marche.prestation.libelle` |
| | 1742-1764 | M | Honoraires : match libellé via `prestation` ou catégorie prestation |
| `DepotFactureClaudeAnalysisService.php` | 127, 156 | L | Mot « prestation » dans prompt OCR (sémantique facture, pas FK) |

### 3.3 Helpers

| Fichier | Rôle |
|---------|------|
| `MarcheDocumentsHelper.php` | `documentsRequisPourPosteType()` = chemin cible ; `documentsRequis(Prestation)` **@deprecated** — fallback catégorie prestation si pas de poste |

### 3.4 Jobs / Observers / Policies

**Aucune** référence à `Prestation` dans `app/Jobs`, `app/Observers`, `app/Policies`.

---

## 4. Frontend Vue (`resources/js/`)

| Composant | `prestation_id` en POST/PUT | Affichage « prestation » | Notes |
|-----------|----------------------------|--------------------------|-------|
| `Marches/Create.vue` | Non — `poste_budgetaire_type_id` | Message obsolète L.486 « Sélectionnez une prestation… » | Formulaire **déjà** sur poste |
| `Marches/Show.vue` | Non — inline `poste_budgetaire_type_id` | Fallback `prestation?.libelle` rare ; workflow TMA « prestation réalisée » | |
| `Marches/Index.vue` | Non | `poste_bilan_libelle ?? prestation?.libelle` | |
| `DepotFactures/DepotFactureSaisieForm.vue` | `poste_budgetaire_type_id` | Label marché `m.prestation` (libellé API) | Compta : `lignes_comptables`, pas prestation |
| `Concessionnaires/Index.vue` | — | Groupement par `groupe.prestation` | Donnée backend legacy |
| `Programmes/Tma.vue` | Route `prestation-realisee` | Statut TMA | Hors dimension prestation |
| `Admin/Nomenclature.vue` | — | `poste.compte_comptable` (CLASSID admin) | |

---

## 5. Routes & API

| Route | Contrôleur | Encore utilisée |
|-------|------------|-----------------|
| `POST programmes/{programme}/tmas/{tma}/prestation-realisee` | `TMAController@marquerPrestationRealisee` | Oui (workflow TMA) |
| CRUD `/admin/nomenclature/*` | `NomenclatureController` | Gère postes ; effets de bord sur `Prestation` à la suppression |
| Marchés Inertia | `MarcheController` | **Pas** d’endpoint CRUD `prestations` |

**Pas de routes API REST dédiées aux prestations** (`routes/api.php` : rien).

---

## 6. Seeders & fixtures

| Fichier | Rôle |
|---------|------|
| `PrestationsSeeder.php` | **57** prestations + **`compte_comptable` GL** par `code_prestation` (aligné LISTES_LOTS / CSV) |
| `PrestationsPostesSeeder.php` | Map `code_prestation` → `poste_budgetaire_type_id` (**~56** codes ; **`499_TAXE_AUTRE` absent**) |
| `DatabaseSeeder.php` | Appelle `PrestationsSeeder` **mais pas** `PrestationsPostesSeeder` |
| `PostesBudgetairesTypesSeeder.php` | 39 postes ; **`compte_comptable` toujours `null`** à la création |

**Factories :** aucune factory `Prestation`.

---

## 7. Code comptable — point le plus critique

### 7.1 Où vit le code comptable « lot / prestation » aujourd’hui ?

| Emplacement | Type de valeur | Utilisé à l’exécution pour APBILL ? |
|-------------|----------------|-------------------------------------|
| **`prestations.compte_comptable`** | Compte GL (ex. `101_ARCHITECTE` → `331453`, `203_GROS_OEUVRE` → `333000`, `505_TMA` → `707201`) | **Non** (stockage + seed uniquement) |
| **`postes_budgetaires_types.compte_comptable`** | Souvent **CLASSID** Intacct (via admin nomenclature) ; nullable au seed | Admin / affichage nomenclature ; migration `2026_05_13_120000` tente lien `intacct_class_id` |
| **`types_factures.code_comptable_defaut`** | GL par défaut (`333000` pour FACTURE/DGD) | **Non** branché sur prefill compta front (voir ci-dessous) |
| **`comptes_comptables.accountno`** | Plan comptable Intacct synchronisé | **Oui** — source **`ACCOUNTNO`** APBILL |

Référentiel CSV `LISTES_LOTS_ENVOL.csv` : **non présent** dans le dépôt ; contenu reproduit dans `PrestationsSeeder::prestationRows()`.

### 7.2 Chaîne APBILL Intacct — origine de `ACCOUNTNO`

```
DepotFacture.ocr_raw.saisie.lignes_comptables[].compte_id
    → CompteComptable.accountno
        → ComptabilisationController::envoyerDepotApbillEtPaiementIntacct()
            → IntacctService::createApBill() / setGlAccountNumber(accountno)
```

- **CLASSID** (analytique programme) : `programme.intacctClass` ou `saisie.analytics_id` — champ séparé `classid` sur chaque ligne APBILL.  
- **Retenues** : comptes paramétrés `ErpComptesComptablesService` (401/445 exclus des lignes charge).

**Prefill front** (`DepotFactureSaisieForm.vue`, `numeroCompteChargeSelonDestination`) :

- Destination `PROGRAMMES` → compte charge **`604000`** (fixe).  
- Frais généraux → patterns libellé catégorie ou `627000`.  

→ Le mapping **prestation → 333000 / 331453** n’alimente **pas** la saisie automatique actuelle.

### 7.3 `postes_budgetaires_types.compte_comptable` — remplissage

| Source | État attendu |
|--------|--------------|
| Seeder dev | **0 / 39** remplis (`null`) |
| Admin nomenclature (`updatePoste`) | Rempli avec **`IntacctClass.classid`** si classe choisie |
| Prod OVH | **À mesurer** (requêtes § 8) — risque de mélange sémantique GL (prestations) vs CLASSID (postes) si on copie naïvement |

### 7.4 Risque de migration GL prestation → poste

| Problème | Détail |
|----------|--------|
| Cardinalité | **N prestations → 1 poste** (ex. 20+ codes travaux → `C_TRAVAUX_VRD` id 20, tous `333000`) |
| Sémantique colonne | Renommer ou ajouter ex. `compte_gl_defaut` sur `postes_budgetaires_types` plutôt que réutiliser `compte_comptable` si celui-ci porte le CLASSID |
| Postes sans équivalent | Prestations sans `poste_budgetaire_type_id` ou codes hors mapping → **SIGNALER**, ne pas supprimer |
| Code manquant seed | `499_TAXE_AUTRE` non mappé dans `PrestationsPostesSeeder` |

**Piste technique :** table de correspondance `prestation_id → poste_budgetaire_type_id` + export des couples `(code_prestation, compte_comptable)` avant merge ; pour chaque poste cible, retenir l’ensemble des GL distincts (alerte si > 1).

---

## 8. Requêtes SQL recommandées sur prod (lecture seule)

À exécuter sur OVH **avant** toute migration destructive :

```sql
-- A. Marchés encore liés à une prestation
SELECT COUNT(*) AS marches_avec_prestation_id
FROM marches WHERE prestation_id IS NOT NULL AND deleted_at IS NULL;

-- B. Marchés sans poste direct (dépendance legacy)
SELECT COUNT(*) AS marches_sans_poste_direct
FROM marches
WHERE poste_budgetaire_type_id IS NULL AND prestation_id IS NOT NULL AND deleted_at IS NULL;

-- C. Prestations sans poste type
SELECT id, code_prestation, libelle, compte_comptable
FROM prestations
WHERE poste_budgetaire_type_id IS NULL;

-- D. Prestations référencées par au moins un marché
SELECT p.id, p.code_prestation, COUNT(m.id) AS nb_marches
FROM prestations p
JOIN marches m ON m.prestation_id = p.id AND m.deleted_at IS NULL
GROUP BY p.id, p.code_prestation;

-- E. Lots techniques (table lots) liés à prestation
SELECT COUNT(*) FROM lots WHERE prestation_id IS NOT NULL;

-- F. Postes types : compte_comptable rempli vs vide
SELECT
  SUM(compte_comptable IS NOT NULL AND compte_comptable <> '') AS remplis,
  SUM(compte_comptable IS NULL OR compte_comptable = '') AS vides
FROM postes_budgetaires_types;

-- G. GL prestations non présents comme accountno Intacct (après sync comptes)
SELECT DISTINCT p.compte_comptable
FROM prestations p
LEFT JOIN comptes_comptables c ON c.accountno = p.compte_comptable
WHERE c.id IS NULL
ORDER BY 1;

-- H. Incohérence : plusieurs GL pour un même poste cible
SELECT p.poste_budgetaire_type_id, COUNT(DISTINCT p.compte_comptable) AS nb_gl_distincts
FROM prestations p
WHERE p.poste_budgetaire_type_id IS NOT NULL
GROUP BY p.poste_budgetaire_type_id
HAVING nb_gl_distincts > 1;
```

---

## 9. Cartographie prestation → poste (seed)

`PrestationsPostesSeeder` (à lancer manuellement aujourd’hui) — regroupements notables :

| Poste type (id seed indicatif) | Exemples `code_prestation` |
|-------------------------------|----------------------------|
| 20 — C_TRAVAUX_VRD | Tous les travaux 201–299, 119, 220 |
| 29 — C_ARCHITECTE | 101_ARCHITECTE |
| 28 — C_MOE_OPC | 102_MOE |
| 23–27 — BET / contrôles | 103*, 104*, 108 |
| 13–19 — VRD concessionnaires | 301–399 |
| 3–11 — Terrain / taxes | 109_ETUDE_DE_SOL, 105, 107*, 401–404 |
| 32, 35–37 — FG / CD | assurances, commissions, marketing, TMA |

**Non mappé dans le seeder :** `499_TAXE_AUTRE` (compte `332100` en base prestation).

**Codes prestation en base prod hors seed :** comparer `SELECT code_prestation FROM prestations` au référentiel — toute ligne orpheline = **signal bloquant**.

---

## 10. Plan de reprise des liens (proposé)

### Phase 0 — Gel & mesure prod

- Exécuter requêtes § 8, exporter CSV des écarts.  
- Corriger manuellement les marchés au mauvais poste (cf. note `poste_budgetaire_type_id = 20` généralisé).  
- Sauvegarder : `prestations`, `marches`, `depot_factures`, mapping GL.

### Phase 1 — Reprise données (sans drop)

1. **Marchés** : pour tout `prestation_id IS NOT NULL` et `poste_budgetaire_type_id IS NULL`, copier `prestations.poste_budgetaire_type_id` puis mettre `prestation_id = NULL` (répéter / compléter migration 2026_05_19).  
2. **Dépôts** : s’assurer que `depot_factures.poste_budgetaire_type_id` est renseigné (colonne directe ou dérivé marché).  
3. **Prestations → poste** : exécuter / compléter `PrestationsPostesSeeder` + règles métier pour `499_TAXE_AUTRE` et codes prod hors référentiel.  
4. **GL** : décision architecture :
   - **Option A** : nouvelle colonne `compte_gl_defaut` sur `postes_budgetaires_types` + alimentation depuis prestations (agrégation + alertes H).  
   - **Option B** : table `poste_budgetaire_type_comptes_gl` (1-n).  
   - **Ne pas** écraser un CLASSID Intacct existant dans `compte_comptable` sans audit.  
5. **Lots** : `UPDATE lots SET prestation_id = NULL` ou suppression table si confirmé inutilisé.

### Phase 2 — Code (compatibilité descendante temporaire)

| Zone | Action |
|------|--------|
| `posteBudgetaireTypePourMarche` | Retirer branche prestation une fois données OK |
| `BilanFinancierService` | Remplacer `join prestations` par `m.poste_budgetaire_type_id` seul ; corriger `buildMarchesDetailsByPoste` (inner join actuel **exclut** marchés sans prestation) |
| `ConcessionnairesController` | `whereHas posteBudgetaireType` + `est_concessionnaire` |
| `TMAController` | Eager `posteBudgetaireType` |
| `SharePointService` | Libellés uniquement depuis poste |
| `DepotFactureController` | Payload : `poste_bilan_libelle` au lieu de `prestation` |
| `NomenclatureController::posteTypeEstUtilise` | Ajouter `marches.poste_budgetaire_type_id` |
| Front | Libellés « poste » / retirer fallbacks `prestation?.libelle` |
| `MarcheDocumentsHelper` | Supprimer chemin deprecated `Prestation` |

### Phase 3 — Schéma (dernier)

Ordre suggéré :

1. Supprimer FK `marches.prestation_id` (après colonne toujours NULL + code sans référence).  
2. Supprimer FK `lots.prestation_id` / table `lots` si abandonnée.  
3. Supprimer `prestations.poste_budgetaire_type_id` puis table `prestations`.  
4. Retirer modèle `Prestation`, seeders, relation `PosteBudgetaireType::prestations()`.

**Test restrict :** tant que `marches.prestation_id` existe avec `restrictOnDelete`, `DELETE FROM prestations` échoue si un marché pointe encore vers la ligne.

---

## 11. Matrice « si DROP aujourd’hui »

| Entité | Orphelins / erreur |
|--------|-------------------|
| Marchés avec `prestation_id` | **DELETE prestation bloqué** (restrict) |
| Marchés seulement `poste_budgetaire_type_id` | OK si code ne lit plus prestation |
| Bilan détail marchés | **Disparaît** des postes (inner join prestation) pour marchés sans prestation_id |
| Concessionnaires | Liste **vide** si filtre prestation inchangé et `prestation_id` null |
| Dépôt facture (libellé) | Perte libellé « prestation » affiché, pas la FK |
| Compta Intacct | **Pas d’impact direct** sur ACCOUNTNO |
| Nomenclature suppression poste | Logique delete `Prestation` obsolète |

---

## 12. Fichiers à toucher (checklist future PR)

**Backend :**  
`Marche.php`, `Prestation.php`, `Lot.php`, `PosteBudgetaireType.php`, `MarcheController.php`, `DepotFactureController.php`, `ConcessionnairesController.php`, `TMAController.php`, `BilanFinancierService.php`, `SharePointService.php`, `NomenclatureController.php`, `MarcheDocumentsHelper.php`

**Front :**  
`Marches/Create.vue`, `Show.vue`, `Index.vue`, `DepotFactures/DepotFactureSaisieForm.vue`, `Concessionnaires/Index.vue`

**Data :**  
Nouvelle migration de backfill + éventuelle colonne GL ; mise à jour `DatabaseSeeder` ; compléter `PrestationsPostesSeeder`

**Hors scope dimension :**  
TMA `prestation_realisee`, catégorie `prestations_sup`, prompts OCR « prestation »

---

## 13. Décisions ouvertes (à trancher métier + finance)

1. Où porter le **GL par défaut** des anciennes prestations sur le **poste** (nouvelle colonne vs table n-n) ?  
2. Faut-il brancher le prefill compta (`604000` fixe) sur le GL poste / type facture ?  
3. Que faire des prestations prod **sans** poste type ou avec **plusieurs GL** pour un même poste ?  
4. Suppression de la table technique `lots` ou conservation sans FK prestation ?

---

*Document généré par audit statique du dépôt. Les comptages prod doivent être validés par les requêtes § 8 sur OVH.*
