---
suivi: 1043
date: 2026-07-29
sujet: Fix GROUP BY only_full_group_by — KPI Facturé axe marché
chantier: budgets-programme
type: fix
statut: poussé
hash: 1bb37488
fichiers:
  - app/Services/BilanFinancierService.php
  - tests/Unit/BilanFinancierFix1043Test.php
  - tools/diag/diag_bilan_1043.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1043_fix_group_by_kpi_marche.md
---

## PROMPT ENVOYÉ

SUIVI #1043 — PHASE 2 correctif. Cause racine #1041 : `buildFactureNv1ByMarche`
GROUP BY sur expression contenant une sous-requête corrélée → MySQL 1055
`only_full_group_by` → catch silencieux → Facturé/dépli à 0 sur 7 programmes.
Back-only, pas de `.vue` / pas de rebuild (parallèle #1042). Commit + push distincts.

## SYNTHÈSE

### Pré-requis git

- `origin/main` au démarrage : `f6451b51` (#1041). Pendant l’exécution, #1042 a été
  poussé (`d7ce8aa1` / docs `5a153700`) — working tree #1043 isolé, sans `public/build/`.

### Choix (a) — LEFT JOIN

Piste retenue : **(a) LEFT JOIN `factures`**, expression
`COALESCE(fa.marche_id, d.marche_id)` en SELECT/WHERE/GROUP BY.

Justification : la plus simple, pas de sous-requête dérivée ni CTE, même sémantique
COALESCE facture→dépôt, compatible `only_full_group_by` (plus de colonne non agrégée
`d.facture_id` dans une sous-requête du GROUP BY). (b)/(c) plus verbeux pour le même résultat.

### Requête AVANT → APRÈS

AVANT (faute) :
```
SELECT COALESCE((SELECT f.marche_id FROM factures f WHERE f.id = d.facture_id LIMIT 1), d.marche_id) AS marche_id,
       SUM(d.montant_ht) AS total_ht
FROM depot_factures d
WHERE COALESCE((SELECT …), d.marche_id) IN (…)
  AND <sqlConditionCompteDansFactureApresNv1Valide>
GROUP BY COALESCE((SELECT f.marche_id FROM factures f WHERE f.id = d.facture_id LIMIT 1), d.marche_id)
```

APRÈS :
```
SELECT COALESCE(fa.marche_id, d.marche_id) AS marche_id,
       SUM(d.montant_ht) AS total_ht
FROM depot_factures d
LEFT JOIN factures fa ON fa.id = d.facture_id
WHERE COALESCE(fa.marche_id, d.marche_id) IN (…)
  AND <sqlConditionCompteDansFactureApresNv1Valide>
GROUP BY COALESCE(fa.marche_id, d.marche_id)
```

Règle NV1 inchangée (source unique `DepotFacture`). `DepotFacture.php` non modifié.

### Audit constructions similaires

| Requête / méthode | Construction | Verdict |
|---|---|---|
| `buildFactureNv1ByMarche` | GROUP BY `sqlMarcheEffectifId` (sous-requête corrélée) | **FAUTIVE** → corrigée LEFT JOIN |
| `buildDepotsHorsMarcheKpiParPoste` | `sqlMarcheEffectifId` / `sqlProgrammeBudgetEffectifId` en JOIN/WHERE ; GROUP BY `pb.id` | **OK** — pas de GROUP BY sur l’expression à sous-requête |
| `buildMarchesByPoste` | GROUP BY `pb.id` | **OK** |
| `buildBudgetsAllouesByPoste` | pas d’agrégat NV1 corrélé | **OK** |
| `DepotFacture::scopeWhereMarcheEffectifIn` | WHERE sur expression (pas GROUP BY) | **OK** |
| `FactureGlobaleListeQuery` | filtre WHERE | **OK** |
| `ProgrammeBudgetAncrageHistoriqueBackfillService` | WHERE (hors bilan runtime) | **OK** (hors périmètre) |

Pourquoi hors-marché fonctionne : GROUP BY porte sur `pb.id` (colonne réelle), les
expressions COALESCE ne sont utilisées qu’en JOIN/WHERE/SUM(CASE).

### Objectif 2 — plus de masquage

`buildMarchesKpiProgramme` : `Log::error` (message + classe + file/line + `getTraceAsString`)
puis `throw $e`. Plus de retour `details_by_poste => []` / `par_poste` vides.

Autre `catch (\Throwable)` listé (non refondu dans ce lot) :
- `buildMarchesByPoste` : `Log::warning` + `return []` (même anti-pattern potentiel).

### Contrôle payload

`tools/diag/diag_bilan_1043.php` — lecture seule, `buildBilanPayload`, assertions OK/ANOMALIE
(prog1 details, postes 15/34/20/17/19/38, HONORAIRES, enveloppes prog2, AXE_MARCHE_MORT).

### Front / build

Aucun `.vue`, aucun rebuild, `public/build/` absent du commit.

### Journal

Entrée `bilan-facture-axe-marche-group-by-2026-07-29` (impact visible 7 programmes).

## DÉPLOIEMENT / TEST

```bash
git pull origin main
find app/ -name "*.php" -exec touch {} +
php artisan optimize:clear
php artisan view:clear
php artisan journal:sync
# pas de migrate / pas de npm / pas de composer / pas de optimize|config:cache|route:cache
php tools/diag/diag_bilan_1043.php
```

Vérifier écran Bilan prog1 HONORAIRES (chevron + montants) et prog2 PUBLICITE/SAV.

## LEÇON

1. `only_full_group_by` est ACTIF en prod : un `GROUP BY` sur une expression contenant une
   sous-requête corrélée est rejeté (erreur 1055) ; un environnement de test qui ne
   reproduit pas ce `sql_mode` ne détecte rien.
2. Un `catch (\Throwable)` qui retourne des structures vides transforme une panne en
   **zéros crédibles** : c’est pire qu’un 500, parce que personne ne le voit.
3. Un `Log::warning` n’est pas surveillé (43 occurrences ignorées) : une panne fonctionnelle
   doit sortir en `error`, et le réflexe de diagnostic doit greper
   `production.(ERROR|WARNING|CRITICAL)`, pas seulement `ERROR`.
