---
suivi: 1524
date: 2026-09-11
sujet: Diagnostic — modification d’une saisie historique refusée par la garde Intacct
chantier: depot-factures / saisie-historique
type: diagnostic
statut: poussé
hash: 50bc2f8b
fichiers:
  - docs/suivi/SUIVI_1524_diag_modification_saisie_historique.md
---

# SUIVI #1524 — PHASE 1 diagnostic : modification d’une saisie historique

## PROMPT ENVOYÉ

PHASE 1 diagnostic. Constat prod dépôt 374 (marché MCH-2026-069, statut
payé, saisie historique) : bandeau Admin ERP ouvert, Enregistrer actif,
clic refusé « Les saisies historiques ne sont pas comptabilisées dans
Intacct. ». Décision : une saisie historique doit être modifiable sur
tous ses champs (pas d’écriture Sage à désynchroniser). Cas d’usage :
12 dépôts RG à 0 % alors que le marché est à 5 % (CP souvent déjà
correct). Pas de reprise auto des 12. Pas de changement des factures
non historiques. Ne pas supprimer le bandeau sans le remplacer. Aucune
migration, aucun code applicatif. Fiche seule.

## BASE GIT

HEAD = `origin/main` = `30ad6e05` (chore hash fiche #1523).
Aucun rebase. Aucun fichier applicatif modifié.

## FICHES LUES

- **#1522 LEÇON** — RG historique absente des états si le miroir
  `retenues_garantie` n’est pas écrit ; MRES lit la table pour le payé,
  pas le JSON. Impact : lever la garde PATCH ne suffit pas à faire
  remonter la RG — il faut `upsertHistorique` (idempotent). Ne pas
  confondre avec l’ajustement manuel marché (hors ce lot, pas de
  reprise auto des 12).
- **#1521 LEÇON** — skips `marche_id` NULL dans
  `RetenueGarantieService` L62-66 / L117-120 et
  `RetenueHistoriqueCollecteService` L39-42 / L114-116. Ici le marché
  est renseigné : ces skips **ne s’appliquent pas**.
- **#1520 LEÇON** — `computeRetenuesGlobales` TTC, sans `marche_id`.
  Conversion HT = lecture MRES (`ttcVersHt`, #1304).
- **#1304 / #1302** — stock RG TTC (`retenues_garantie.montant_retenu`) ;
  MRES convertit à la lecture. Plafond libération =
  `etats.rg.montant_retenu` HT.
- **#1308** — CP historique HT natif (`depot_facture_retenues_historiques`
  + `marche_cp_historiques`). Ne pas reconvertir.
- **#1275 LEÇON** — post-`estComptabiliseeOuAuDela` (dont **payée**) :
  `canEditDepot` = GS_ADMIN_ERP seul (#658). Impact : le formulaire
  ouvert sur un historique payé suppose Robin / Admin ERP ; COMPTA
  verrait lecture seule tant que ce cran n’est pas spécialisé.
- **#1273** — bandeau déjà porté dans le formulaire via
  `edit_override_*`. Unification URL hors cause.
- **#1450** — `logHistoriqueIfSaisieChanged` sur PATCH dépôt.

Aucune fiche `docs/suivi/` ne documente le texte exact de la 404
Intacct. Origine git : `d5edb874` (historique hors file Sage) puis
`5ef9cb60` (fix #138 : Enregistrer post-engagement → PATCH compta).

---

## 1. OÙ EST LA GARDE

Texte exact (trois copies, même phrase sauf rejet) :

**A. Lookup file compta / Intacct** —
`ComptabilisationController::findDepotComptabilisation` L127-139 :

```
abort_if((bool) $depot->saisie_historique, 404, 'Les saisies historiques ne sont pas comptabilisées dans Intacct.');
```

Appelée par : `data` L201, `envoyerIntacct` L612,
`envoyerAvoirIntacct` L648, `comptabiliser` L665,
`executerComptabilisationApbill` L743 (donc
`envoyerIntacctCompta` / `envoyerIntacctPaye` /
`envoyerIntacctComptaPourPaiement`).

**B. PATCH enregistrement compta** — `update` L212-218 :

```
$depot = DepotFacture::query()->where('uuid', $uuid)->firstOrFail();
abort_if((bool) $depot->saisie_historique, 404, 'Les saisies historiques ne sont pas comptabilisées dans Intacct.');
```

Route : `PATCH /factures/comptabilisation/{uuid}`
(`routes/web.php` L442). **C’est le chemin du constat prod.**

**C. Filet avant envoi Sage** —
`reponseSiComptaNonPretPourAction` L420-424 :

```
if ((bool) $depot->saisie_historique) {
    return response()->json([
        'message' => 'Les saisies historiques ne sont pas comptabilisées dans Intacct.',
    ], 422);
}
```

Appelée par `envoyerIntacct` L614, `comptabiliser` L671,
`executerComptabilisationApbill` L749. **Pas** par `update()`.

**D. Rejet compta** — `rejeter` L306, message **différent** :
« Les saisies historiques ne sont pas concernées. »

**Hors de cette phrase :**

- `applyDepotSaisieFromValidated` L1563-1566 : « Facture comptabilisée —
  consultation seule… » si payée **et** pas Admin ERP. N’invoque pas
  Intacct.
- `DepotFactureController::update` L1879-1946 : **aucune** garde
  `saisie_historique`. Un PATCH `/depot-factures/{uuid}` (L426)
  irait jusqu’à `apply` + `logHistoriqueIfSaisieChanged`.
- `valider` / `validerHistorique` : pas ce message.
  `validerHistorique` L2710 refuse seulement un 2ᵉ « marquer payé ».

Duplication : **oui**, A+B+C (même texte). B est collatéral : #138 a
branché Enregistrer post-`facture_id` sur le PATCH compta, qui portait
déjà l’abort historique destiné à Sage.

Front qui déclenche B :

`DepotFactureSaisieForm.vue` `depotUpdateUrl` L7245-7249 :

```
if (props.comptabilisationMode || workflowDejaEngage.value) {
    return `/factures/comptabilisation/${props.depot.uuid}`;
}
```

`workflowDejaEngage` L3860 = `facture_id != null`. Un historique
**payé** a une facture (`validerHistorique` L2793) → Enregistrer =
PATCH compta → 404 L218. Toast `saveDraftAjax` L7310 : `d?.message`.

---

## 2. CE QU’ELLE PROTÈGE RÉELLEMENT

### 2.1 Modification vs envoi Sage

| Copie | Protège | Effet sur le constat |
|-------|---------|----------------------|
| A `findDepotComptabilisation` | entrée file Sage (GET data compta, APBILL, clôture `comptabiliser`) | correct |
| C `reponseSiComptaNonPretPourAction` | 422 avant APBILL / clôture | correct, doublon de A |
| B `update` PATCH | **toute** persistance saisie dès que le front tape cette URL | **excessif** |

B refuse la **modification des données**, pas un envoi Sage. `update`
L280-283, **après** l’abort, appellerait
`applyDepotSaisieFromValidated` + `logHistoriqueIfSaisieChanged` —
zéro Intacct.

### 2.2 Un historique peut-il déclencher Intacct autrement ?

**Non** sur les chemins APBILL / APPYMT prévus.

Preuves :

- `DepotFacture::estEligibleComptabilisation` L327-328 : si
  `saisie_historique` → `false`. Scope `eligibleComptabilisation`
  L375 `where('saisie_historique', false)`.
- `promouvoirPourComptabilisation` L354-356 : no-op si historique.
- `executerComptabilisationApbill` L743 : passe par A → 404 avant
  tout client Intacct.
- `confirmerPaiementApresCompta` (APPYMT) L725 : **pas** A, mais
  exige un APBILL préalable (`intacct_bill_id`). Un historique
  n’en a pas (`validerHistorique` ne pose pas de bill ;
  `upsertHistorique` L134 `intacct_bill_id` null).
- Commentaire mort L1682 (« historique marqué payé après APBILL ») :
  antérieur à `d5edb874`, contredit par A.

`intacct_bill_id` non vide ferait skipper `predireHistorique` L180-182
et `predire` CP L104-106. Hors cas 374 (historique hors Sage).

### 2.3 Régression visée

`d5edb874` : les historiques ne doivent **pas** entrer en
`a_comptabiliser` ni dans Intacct (statut `paye` ERP, bilan/KPI oui).
A et C = cette intention.

`5ef9cb60` (ticket interne #138, pas de fiche `docs/suivi/`) : ne plus
réinjecter le workflow via `valider` si `facture_id` ; Enregistrer
post-engagement → PATCH compta pour **préserver le statut**. Avant ce
commit, `update()` passait par `findDepotComptabilisation` (donc A).
#138 **sort** `update()` de A (pour que les factures Sage engagées
s’enregistrent sans être « éligibles compta ») **et recopie** l’abort
historique en L218. Héritage conscient du filet Sage, appliqué au
mauvais verbe HTTP (PATCH données, pas POST Intacct).

Pas de fiche SUIVI pour le texte 404. La garde Sage (A+C) est légitime ;
B est de trop.

---

## 3. LA CONTRADICTION AVEC LE BANDEAU

### 3.1 Où / quelle condition

Constante `FactureAdminErpEditOverride::WARNING_BANNER` L16.

Active ssi `isActive` L31-33 = `userIsAdminErp` (L18-20 →
`User::isAdminERP()` = `GS_ADMIN_ERP` **dans** `m365_groups`, brut)
**et** `FactureStatutService::estComptabiliseeOuAuDela`.

Statuts normalisés L64-71 : `comptabilisee`, `mise_en_paiement`,
`en_paiement`, **`payee`**. Dépôt historique `statut = paye`
(`finaliserStatutHistoriquePaye` L2921-2926) → `payee` (L24 RAW_GROUPS)
→ bandeau **sans** distinguer Sage / hors Sage.

Payload : `buildSaisiePayload` L4299-4301 (et copie L4418-4420).

Front : `DepotFactureSaisieForm.vue` L8391-8396 si
`editOverrideAdmin`. Modal avant save L178-190 /
`confirmAdminErpOverrideIfNeeded`.

Formulaire ouvert : `canEditDepot` L4617-4623, même cran payée →
Admin ERP seul. `workflow_modification.can_enregistrer` L4525 =
`canEditDepot`. Enregistrer actif = cohérent avec le bandeau, **pas**
avec B.

### 3.2 Qui est de trop

Deux objets collés par erreur :

1. **Bandeau Sage** — texte faux pour un historique (aucune bill).
   À **remplacer**, pas supprimer (garde-fou métier).
2. **Abort PATCH B** — de trop. L’enregistrement ERP n’est pas un
   envoi Intacct. A et C restent.

Ni le bandeau (une fois recalé) ni l’ouverture du formulaire ne sont
de trop au regard du besoin. C’est B qui contredit A/C et le métier.

---

## 4. CE QUI SE PASSE SI ON LÈVE LA GARDE B

Hypothèse : abort L218 retiré **ou** `depotUpdateUrl` renvoie
`/depot-factures/{uuid}` si `saisie_historique`. A et C inchangés.
Admin ERP (cas prod).

`update` compta L280-283 ou `DepotFactureController::update` L1933-1936
→ `applyDepotSaisieFromValidated(..., false)` (ne remet pas
`en_cours_saisie`).

### 4.1 Dépôt / JSON / facture

`apply` écrit overlay `ocr_raw.saisie` (dont
`retenue_garantie_pct`) + colonnes via
`appendDirectFkColumnsToUpdates` + `depot->update` L1833.
Si `facture_id` : `syncFactureIdentiteDepuisValidated` L1846,
`FactureMontantsDepotSynchronizer` L1847,
`syncFactureLieeDepuisDepot` L1848 (`declencherRecalcul: true` →
`recalculerMarchesEtBilan` L475-488 = `Marche::recalculer` + bilan
agrégé). **Pas** `mettreAJourCalculsMarche` (seulement
`validerHistorique` L2897).
`normaliserRetenuesSelonPourcentagesMarche` L5732 : **inactif** sur
`STATUT_PAYE` (`depotRetenuesPeuventEtreNormalisees` L5364-5376). Un
Enregistrer ne remplira **pas** tout seul le 0 % par le 5 % marché
(pas de reprise auto des 12). Il ne zéroise que si le % marché est 0.

### 4.2 Miroirs retenues — **pas** alimentés par apply

`validerHistorique` L2882-2890 appelle
`RetenueHistoriqueCollecteService::collecter($depot, 'cp')` et
`RetenueGarantieService::upsertHistorique`. **Aucun** autre appelant
dans `DepotFactureController` (grep unique L2883 / L2888).

`applyDepotSaisieFromValidated` ne les appelle pas.

Skips #1521 `marche_id` NULL : **inactifs** si marché effectif
renseigné (`upsertHistorique` L117-120, `collecter` L39-42). 374 OK.

Donc : lever B seul → JSON + colonnes + facture + agrégats marché/bilan
montants facture. **Pas** `retenues_garantie`, **pas**
`depot_facture_retenues_historiques`. MRES RG payée lit la table
L87-112, pas le JSON (le JSON n’alimente que l’« en cours » L124-155).
La RG 0 % → 5 % **resterait invisible** aux états tant que
`upsertHistorique` n’est pas rejoué.

`upsertHistorique` L122 : `updateOrCreate` sur `facture_id` —
rejeu = **mise à jour**, pas de doublon. Si `%` reste 0 :
`predireHistorique` L216-217 skip `pct_rg_nul` (cas actuel des 12).
Si on passe à 5 % : action `creer` (pas de ligne) ou `deja_present`
(ligne existante, montants écrasés). Passer de 5 % à 0 % : skip, **la
ligne existante n’est pas effacée** (trou PHASE 2).

CP : `collecter` idempotent `(depot_facture_id, type_retenue)` L44-48.
`deja_present` réécrit aussi. validerHistorique ne collecte que `'cp'`,
pas RG via ce service.

Commandes filet (pas nécessaires si PHASE 2 appelle les services au
PATCH) : `retenues-garantie:collecter-historique --apply`
(`CollecterRetenuesGarantieHistoriqueCommand` L13 / L67-68, y compris
`deja_present`) ; `compte-prorata:collecter-historique`.

### 4.3 États marché / avancement / libération

MRES `pourMarche` : lecture seule, requêtes à l’affichage. Après
écriture miroir, le prochain chargement onglet Retenues / Synthèse
voit le nouveau cumul. `Marche::recalculer` (si apply) = totaux
facture marché, pas le plafond RG.

Plafond libération : `DemandeLiberationRgService::plafondPourMarche`
L46-63 = `etats.rg.montant_retenu` MRES (HT). Recalcul **à la
demande**, pas une colonne stockée. Dès que `retenues_garantie`
est à jour, le plafond suit. Sans `upsertHistorique`, inchangé.

### 4.4 Sage — certitude

**Aucun** appel APBILL / APPYMT / Graph. apply n’instancie pas
`IntacctService`. A et C restent devant `executerComptabilisationApbill`.

---

## 5. MODIFICATION DES RETENUES EN PARTICULIER

Changer le taux RG d’un historique **déjà payé** :

1. JSON `ocr_raw.saisie.retenue_garantie_pct` (apply).
2. Montant TTC = `EcrituresComptablesCalculator::computeRetenuesGlobales`
   (`app/Services/DepotFacture/EcrituresComptablesCalculator.php`
   L282-310), appelé par `calculerRgTtcHistorique` L337-361. Formule :
   somme HT×(1+TVA notionnelle) + finitions + CIE, **grossie** par
   `/(1 − Σ%)`, puis `base × rgPct / 100`. Ce n’est pas « % × HT brut ».
   Rejouée à chaque `upsertHistorique` (lit le JSON à jour).
3. Conversion HT = **lecture** MRES L226-228 / L235
   (`MarcheSyntheseFinanciereService::ttcVersHt`). Rejouée à chaque
   affichage, pas à l’enregistrement.
4. Remontée états : **non automatique** au seul PATCH apply. Il faut
   `upsertHistorique` (ou la commande #971). Pas une 2ᵉ collecte
   artisan obligatoire si PHASE 2 l’appelle au save.
5. Doublon : non, `updateOrCreate` `facture_id`. Rejeu = écrasement.
6. Plafond libération : oui, dès MRES relit la table (pas de job).

Ne pas lancer une reprise de masse des 12 (décision). La correction
papier → saisie dépôt → `upsertHistorique` sur **ce** dépôt.

---

## 6. DROITS

Lecture brute : `User::m365GroupNames()` L119-121.
`hasAnyM365Group` L146-152 et `Gate::before`
`AppServiceProvider` L69-74 : GS_ADMIN_ERP → tout vrai.

Aujourd’hui sur un historique **payé** :

| Contrôle | Groupes | Piège |
|----------|---------|-------|
| Bandeau + form ouvert | `isAdminERP()` / `m365_groups` contient GS_ADMIN_ERP | voulu #658 pour Sage-payé ; trop étroit pour historique |
| `canEditDepot` L4617-4623 | Admin ERP seul si `estComptabiliseeOuAuDela` | COMPTA = lecture seule |
| `DepotRetenuesNv1Lock::userPeutModifierRetenuesEtMontants` L42-48 | brut `GS_ADMIN_ERP` si payée | COMPTA 422 même si B levée |
| `canValiderHistoriquePaye` L4745-4748 | `hasAnyM365Group` DSI/COMPTA/DG | **piège** Admin |
| `canSetSaisieHistorique` L4736-4738 | `true` | pose du flag à la création, pas l’édition payée |

Proposition PHASE 2 (historique **uniquement**, `saisie_historique = 1`,
`intacct_bill_id` vide) — `in_array` sur `m365GroupNames()` :

- `GS_COMPTABILITE` — corriger les reprises (12 dépôts) ;
- `GS_ADMIN_ERP` ;
- `GS_DIRECTION_GENERALE` (même intention que marquer payé, en brut).

Pas GS_DSI par défaut (aligné édition post-Sage #658).
Ne **pas** élargir `estComptabiliseeOuAuDela` (trop de consommateurs).
Exception locale : `canEditDepot` + `DepotRetenuesNv1Lock` si
historique hors Sage. Factures non historiques : cran #658 inchangé.

---

## 7. TRAÇABILITÉ

Aujourd’hui, **si** le PATCH aboutit (`DepotFactureController::update`
L1934-1936 ou compta `update` L281-283) :

- `FactureHistoriqueSaisie::snapshotFromDepot` + `diff` →
  `logHistorique('modifie', avant, apres)` (`Historisable` L14-37) →
  table `facture_historique` (`depot_facture_id`, `action`,
  `champs_modifies`, `user_id`, commentaire override L5992-5993).
- Snapshot : tout `ocr_raw.saisie` (donc `retenue_garantie_pct`) ;
  post-engagement, aussi colonnes `_depot`
  (`colonnesDirectesTracables` L15-37, dont montants, marché).

`entity_field_audits` : foncier uniquement
(`FieldAuditService`, `ActeAcquisitionService`). **Pas** les dépôts.

Le constat prod : B coupe **avant** le log → **aucune** trace du clic
refusé. PHASE 2 : conserver `logHistoriqueIfSaisieChanged` ; commentaire
dédié historique (pas le texte Sage #658). Option : action
`modifie_historique` pour filtrer. Suffisant ; pas d’audit field
foncier à étendre.

---

## 8. VOLUME — SQL à coller en prod

Pas d’accès MySQL ici. Requêtes dans la SYNTHÈSE.

---

## PÉRIMÈTRE PHASE 2 (proposition, pas un fix)

Un lot. Pas de migration. Pas de reprise des 12. Pas de touche aux
non-historiques. A et C conservés.

1. Enregistrer un historique engagé : PATCH
   `/depot-factures/{uuid}` **ou** saut de l’abort L218 seulement
   (A/C inchangés). Front `depotUpdateUrl` : si
   `saisie_historique`, ne plus forcer l’URL compta.
2. Après apply : `upsertHistorique` + `collecter(..., 'cp')` (comme
   `validerHistorique` L2882-2890). Idempotent. Filet 0 % → supprimer
   la ligne RG si on autorise de repasser à 0.
3. Droits §6 (brut). Exception historique hors Sage dans
   `canEditDepot` et `DepotRetenuesNv1Lock`.
4. Remplacer le bandeau Sage par un bandeau historique (ERP only,
   pas d’écriture Sage — formulation positive). Garder un bandeau.
5. Journal `facture_historique` déjà câblé — vérifier le commentaire.
6. Tests source : PATCH historique payé 200 + miroir RG ; PATCH
   non-historique inchangé ; APBILL historique toujours 404.
7. Journal utilisateur (édition historique possible). Rebuild Vite
   (`DepotFactureSaisieForm.vue` zone commune — rebase `origin/main`
   avant build).

---

## DÉPLOIEMENT-TEST

```text
git pull origin main
```

Pas de migrate, journal:sync, npm, optimize. Coller les SQL du §8.
Dépôt 374 : après PHASE 2, Enregistrer RG 5 % → ligne
`retenues_garantie` + onglet Retenues MCH-2026-069.

## LEÇON

Une garde « pas d’Intacct » est juste sur la **file Sage** et fausse
dès qu’on recycle le PATCH compta pour tout dépôt à `facture_id`
(#138). Un historique payé est `estComptabiliseeOuAuDela` donc bandeau
Sage + Admin ERP, alors qu’il n’y a pas de bill. Lever l’abort sans
rejouer `upsertHistorique` écrit le JSON et laisse les états à 0 €
(MRES lit `retenues_garantie` pour le payé).

---

```markdown
## SUIVI #1524 — SYNTHÈSE diagnostic modification saisie historique

### Verdict
Contradiction : le bandeau Admin ERP ouvre le form (payée =
estComptabiliseeOuAuDela + GS_ADMIN_ERP) mais Enregistrer PATCH
/factures/comptabilisation/{uuid} (workflowDejaEngage = facture_id)
et ComptabilisationController::update L218 abort 404
« Les saisies historiques ne sont pas comptabilisées dans Intacct. »
Cette 404 protège l’enregistrement ERP, pas Sage. apply ensuite
n’appelle pas Intacct. La garde Sage légitime est
findDepotComptabilisation L133 et reponseSiComptaNonPretPourAction
L420-424 (APBILL / comptabiliser). Origine : d5edb874 (hors file
Sage) + 5ef9cb60 (#138 Enregistrer post-engagement → PATCH compta).
Pas de fiche docs/suivi/ pour le texte.

### 1. Garde
Unique phrase, 3 sites ComptabilisationController :
L133 (findDepotComptabilisation), L218 (update PATCH = constat),
L422 (422 avant Sage). Rejet L306 autre texte. apply / valider /
validerHistorique / PATCH depot-factures : pas cette phrase.
Front : DepotFactureSaisieForm.vue depotUpdateUrl L7245-7249.

### 2. Ce qu’elle protège
B (PATCH) = modification données. A+C = envoi Sage. Historique
ineligible (estEligibleComptabilisation L327, promouvoir L354).
APBILL passe par A L743. APPYMT
confirmerPaiementApresCompta L722-725 : pas d’abort historique, mais
exige facture_id + bill préalable. Aucun Intacct si on lève B.
Intention d5edb874 : pas a_comptabiliser.

### 3. Bandeau
WARNING_BANNER FactureAdminErpEditOverride L16. isActive L31-33.
Payload buildSaisiePayload L4299-4301. Vue L8391-8396. Paye
historique = payee L64-71 → bandeau Sage à tort. De trop : abort
PATCH B + texte Sage du bandeau. À garder : A/C + un bandeau
recalé + form éditable.

### 4. Si on lève B
apply écrit JSON + colonnes + sync facture + recalcul marché/bilan
montants. Pas upsertHistorique / collecter CP (seulement
validerHistorique L2882-2890). Skips marche_id NULL #1521 inactifs
si marché présent. Sage : aucun appel. MRES RG payée = table
retenues_garantie L87 — JSON seul ne suffit pas.

### 5. Retenues
% saisi → computeRetenuesGlobales TTC (base TTC notionnelle grossie
par 1-Σ%, pas %×HT). HT = ttcVersHt à la lecture MRES L226 /
MarcheSyntheseFinanciereService L902. Remontée états : appeler
upsertHistorique (updateOrCreate facture_id, pas de doublon) ou
php artisan retenues-garantie:collecter-historique --apply.
Plafond libération = MRES live. 0 % → skip, ne supprime pas une
ligne existante.

### 6. Droits
Aujourd’hui payée → Admin ERP seul (canEditDepot L4617,
DepotRetenuesNv1Lock L42 brut). Proposition historique hors Sage :
GS_COMPTABILITE + GS_ADMIN_ERP + GS_DIRECTION_GENERALE via
m365GroupNames() brut. Jamais hasAnyM365Group (canValiderHistoriquePaye
L4747 est le piège). Ne pas élargir estComptabiliseeOuAuDela.

### 7. Trace
facture_historique via logHistoriqueIfSaisieChanged si PATCH aboutit
(saisie + colonnes _depot). entity_field_audits = foncier seulement.
Prod : 404 avant le log. PHASE 2 : garder le log, commentaire
historique (pas le texte Sage).

### PHASE 2
Rerouter Enregistrer historique vers PATCH depot (ou sauter L218
seulement). A/C inchangés. Après apply : upsertHistorique +
collecter CP. Droits §6. Remplacer le bandeau (ne pas le supprimer).
Pas de reprise auto des 12. Pas de changement non-historique.
Journal + rebuild Vite (SaisieForm, rebase origin/main).

### SQL 1 — volume saisies historiques par statut
SELECT d.statut, COUNT(*) AS nb,
       ROUND(SUM(COALESCE(d.montant_ht, 0)), 2) AS montant_ht,
       SUM(d.facture_id IS NOT NULL) AS nb_avec_facture,
       SUM(d.intacct_bill_id IS NOT NULL AND TRIM(d.intacct_bill_id) <> '') AS nb_avec_bill
FROM depot_factures d
WHERE d.deleted_at IS NULL
  AND (
    d.saisie_historique = 1
    OR d.type_source = 'historique'
    OR JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.saisie_historique')) IN ('true', '1')
  )
GROUP BY d.statut
ORDER BY nb DESC;

### SQL 2 — retenues JSON (RG / CP / OPC / finitions / CIE)
SELECT
  SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_garantie_pct')) AS DECIMAL(10,4)) > 0.009) AS nb_rg_pct,
  SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.compte_prorata_pct')) AS DECIMAL(10,4)) > 0.009) AS nb_cp_pct,
  SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_opc_pct')) AS DECIMAL(10,4)) > 0.009) AS nb_opc_pct,
  SUM(ABS(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_finitions_montant')) AS DECIMAL(15,2))) > 0.009) AS nb_finitions,
  SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.montant_cie')) AS DECIMAL(15,2)) > 0.009) AS nb_cie
FROM depot_factures d
WHERE d.deleted_at IS NULL
  AND (
    d.saisie_historique = 1
    OR d.type_source = 'historique'
  );

### SQL 3 — RG à 0 % sur historique payé (cas 12 dépôts / 374)
SELECT d.id, d.uuid, d.statut, d.marche_id, m.reference AS marche_ref,
       m.retenue_garantie_pct AS marche_rg_pct, m.compte_prorata_pct AS marche_cp_pct,
       d.montant_ht, d.montant_ttc, d.intacct_bill_id,
       JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_garantie_pct')) AS saisie_rg_pct,
       JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.compte_prorata_pct')) AS saisie_cp_pct,
       rg.id AS rg_id, rg.montant_retenu AS rg_ttc, rg.source AS rg_source
FROM depot_factures d
LEFT JOIN marches m ON m.id = COALESCE(
  (SELECT f.marche_id FROM factures f WHERE f.id = d.facture_id LIMIT 1),
  d.marche_id
)
LEFT JOIN retenues_garantie rg ON rg.facture_id = d.facture_id
WHERE d.deleted_at IS NULL
  AND (d.saisie_historique = 1 OR d.type_source = 'historique')
  AND d.statut = 'paye'
  AND COALESCE(CAST(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_garantie_pct')) AS DECIMAL(10,4)), 0) <= 0.009
ORDER BY d.montant_ht DESC;

### SQL 4 — dépôt 374 (contrôle unitaire)
SELECT d.id, d.uuid, d.statut, d.saisie_historique, d.type_source, d.deja_reglee,
       d.facture_id, d.marche_id, d.intacct_bill_id, d.montant_ht, d.montant_ttc,
       JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.retenue_garantie_pct')) AS rg_pct,
       JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.compte_prorata_pct')) AS cp_pct
FROM depot_factures d
WHERE d.id = 374;
```
