---
suivi: 1033
date: 2026-07-29
sujet: Backfill ancrage historique hors marché → programme_budget_id (lot 2c)
chantier: budgets-programme
type: feature
statut: déployé et APPLIQUÉ en prod le 29/07 (--apply --programme=2 : 17 dépôts + 17 factures rattachés, 1 enveloppe « Historique » créée, overlap = 0, idempotence vérifiée ; depot#1206 HERMIONE volontairement non traité, en_cours_saisie)
hash: 6e7fb4c3
fichiers:
  - app/Console/Commands/BudgetsBackfillAncrageHistoriqueCommand.php
  - app/Services/ProgrammeBudgetAncrageHistoriqueBackfillService.php
  - tests/Unit/ProgrammeBudgetAncrageHistoriqueBackfillTest.php
  - docs/suivi/SUIVI_1033_backfill_ancrage_historique.md
---

## PROMPT ENVOYÉ

SUIVI #1033 — lot 2c migration données : commande artisan idempotente
`budgets:backfill-ancrage-historique` (dry-run / --apply / --programme=N).
Backfill `programme_budget_id` sur dépôts hors marché avec `poste_budgetaire_type_id`,
sans toucher bilan ni formulaire. Zéro perte : uniquement NULL→valeur sur programme_budget_id
(depot + facture), création enveloppes « Historique » si besoin. Pas de journal (#412).

## SYNTHÈSE

### Pré-requis git
- HEAD avant lot : `a589e5ac chore(suivi): renseigne hash #1032 (2a95008d)`

### Implémentation
- **Commande** : `app/Console/Commands/BudgetsBackfillAncrageHistoriqueCommand.php`
  - Signature : `budgets:backfill-ancrage-historique {--apply} {--programme=}`
  - Dry-run par défaut ; `QUEUE_CONNECTION=sync` forcé en handle.
- **Service** : `app/Services/ProgrammeBudgetAncrageHistoriqueBackfillService.php`
  - Sélection : marché effectif NULL, poste NOT NULL, programme NOT NULL, budget effectif NULL,
    hors rejet/archive/soft-delete (même critères que diag #1032 B2).
  - Groupement (programme_id, poste_budgetaire_type_id) :
    - 1 enveloppe active → rattachement ;
    - 0 → création via `ProgrammeBudgetService::creer()` (libellé « Historique », montant_ht=0,
      commentaire « créée par backfill #1033 », révision append-only) ;
    - ≥2 → ARBITRAGE MANUEL (aucune écriture).
  - Écriture : **DB::table** update `programme_budget_id` uniquement si NULL (dépôt + facture liée).
    Pas de modification de `poste_budgetaire_type_id` ni autres colonnes.
  - Transaction unique en --apply ; idempotent (2e passage = 0 rattachement).
  - Post-apply : contrôle overlap C3 #1032 + recompte éligibles (hors arbitrage = 0).

### Observers / side effects
- **Pas de DepotFactureObserver** enregistré.
- **FactureObserver** : log MarcheHistorique seulement si `marche_id` renseigné et clés trackées
  (`statut`, montants, `reference`) — non concerné par `programme_budget_id`.
- Écritures factures via **DB::table** (pas d’événement Eloquent) pour neutraliser tout effet de bord.
- Aucun appel HTTP / Sage / mail.

### Journal #412
**NON** — aucun impact UI visible (backfill données seul).

### Fichiers non touchés (garde-fous)
BilanFinancierService, Bilan/Index.vue, DepotFactureSaisieForm.vue, DepotFactureController,
ComptabilisationController, assertRattachement #1029/#1031, `estConsomme()` stub.

### Vérifs techniques (sorties brutes)

php -l app/Services/ProgrammeBudgetAncrageHistoriqueBackfillService.php
→ No syntax errors detected in app/Services/ProgrammeBudgetAncrageHistoriqueBackfillService.php

php -l app/Console/Commands/BudgetsBackfillAncrageHistoriqueCommand.php
→ No syntax errors detected in app/Console/Commands/BudgetsBackfillAncrageHistoriqueCommand.php

php -l tests/Unit/ProgrammeBudgetAncrageHistoriqueBackfillTest.php
→ No syntax errors detected in tests/Unit/ProgrammeBudgetAncrageHistoriqueBackfillTest.php

head -3 service :
<?php
namespace App\Services;
(final class ProgrammeBudgetAncrageHistoriqueBackfillService)
wc-l service = 360

head -3 command :
<?php

namespace App\Console\Commands;
(class BudgetsBackfillAncrageHistoriqueCommand)
wc-l command = 170

DI + Reflection (php -r bootstrap) :
DI OK signature=budgets:backfill-ancrage-historique
ReflectionClass OK: App\Services\ProgrammeBudgetAncrageHistoriqueBackfillService
Service instantiate OK

Tests : `php artisan test --filter=ProgrammeBudgetAncrageHistoriqueBackfillTest`
→ 4 skipped (pdo_sqlite absent sur l’environnement Cursor) — validation réelle = dry-run prod.

## DÉPLOIEMENT / TEST

1. `git pull origin main` (OVH, racine projet)
2. **Pas** de `migrate`, `npm`, `composer`, `optimize`
3. Purge OPcache (PHP ajouté) :
   `find app/ -name "*.php" -exec touch {} +`
4. Dry-run global :
   `php artisan budgets:backfill-ancrage-historique`
5. Dry-run programme 2 (recommandé avant apply) :
   `php artisan budgets:backfill-ancrage-historique --programme=2`
6. Apply (transaction unique) :
   `php artisan budgets:backfill-ancrage-historique --apply`
   ou ciblé : `php artisan budgets:backfill-ancrage-historique --apply --programme=2`
7. Vérifier rapport post-apply : overlap = 0 ; éligibles hors arbitrage = 0
8. Idempotence : relancer --apply → « aucun nouveau rattachement »

## LEÇON

Le lot 2c ne modifie que `programme_budget_id` (double ancrage #1029) sur dépôts hors marché
historiques ; `poste_budgetaire_type_id` reste intact pour le lot 2d. Création enveloppe via
ProgrammeBudgetService (révisions) ; rattachement via DB façade pour éviter observers.
Préalable lot 3 BILAN : exécuter ce backfill avant d’agrégérer l’axe enveloppe.
