---
suivi: 1952
date: 2026-10-16
sujet: Doublon probable recalculé et bloquant (NV1, compta, paiement)
chantier: factures / doublons
type: fix
statut: poussé
hash: f476b2fcb
fichiers:
  - app/Support/NumeroFactureFournisseurNormalizer.php
  - app/Services/DepotFactureDuplicateDetector.php
  - app/Services/DepotFactureDoublonService.php
  - app/Exceptions/DoublonProbableException.php
  - app/Console/Commands/FacturesDoublonsRecalculCommand.php
  - app/Services/FactureElectronique/FactureElectroniqueImport.php
  - app/Services/DepotFactureClaudeAnalysisService.php
  - app/Services/Outlook/OutlookDepotCreation.php
  - app/Http/Controllers/DepotFactureController.php
  - app/Http/Controllers/ComptabilisationController.php
  - app/Http/Controllers/FactureController.php
  - app/Services/PaiementAppymtExecutionService.php
  - app/Services/PaiementAppymtBatchService.php
  - app/Services/FactureVirementExecuteService.php
  - database/migrations/2026_10_16_150000_add_montant_ttc_index_for_doublons.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - resources/js/Pages/DepotFactures/DepotFactureSaisieForm.vue
  - resources/js/Pages/Factures/Paiement/Index.vue
  - resources/js/Components/Comptabilisation/ComptabilisationPaiementDialog.vue
  - public/build/
  - tests/Unit/DepotFactureDuplicateDetectorTest.php
  - tests/Feature/DepotFactureDoublon1952Test.php
  - tests/Feature/FactureElectronique/FactureElectroniqueImport1845Test.php
  - tests/Support/SchemaFacturesDoublon.php
---

# SUIVI #1952 — Doublon probable recalculé et bloquant

## Fiches lues

- #1949 LEÇON : `doublon_detecte` était un instantané OCR (et import), jamais relancé. 1214 / 2469 non rapprochées car le n° « 0938M-26 SIT 4 » n’était pas égal à « 0938M-26 » après `normalizeForCompare`. Aucun contrôle avant paiement.
- #1413 (via #1949) : la case « ce n’est pas un doublon » est une empreinte. Si l’empreinte change, l’alerte se réarme. Ne pas l’écraser quand elle couvre encore le rapprochement.
- #1444 (via #1949) : le bandeau, Comparer et Voir dépendent de `v-if="depot.doublon_detecte"`. Ce `v-if` n’a pas été modifié.
- #1845 (via #1949) : « l’électronique prime » est la direction du marquage, pas le critère. Le critère est désormais le même moteur 3/4.

## PROMPT ENVOYÉ

PHASE 2 correctif #1952, suite du diagnostic #1949. Doublon probable = au moins 3 critères sur 4. Comparaison du n° : égalité, suffixe SIT/SITUATION, ou inclusion si le plus court fait au moins 6 caractères. Recalcul à chaque changement des 4 champs, à la demande NV1, avant compta, avant paiement. Bloquant, déblocage uniquement par la case #1413. Même moteur OCR, Outlook, factures électroniques. Commande `factures:doublons-recalcul` en aperçu, `--appliquer` limité aux deux colonnes. Livrer sur main.

## SYNTHÈSE

Doublon probable = score ≥ 3 sur fournisseur, n° de facture, date de facture, montant TTC (± 0,01, arrondi au centime). Le socle historique « n° + montant seuls » ne suffit plus.

Comparaison du n° (`NumeroFactureFournisseurNormalizer::numerosConcordent`, après `normalizeForCompare`) :

1. égalité ;
2. égalité après retrait du suffixe `(SITUATION|SIT)\d*` (SITUATION avant SIT) ;
3. inclusion si le plus court fait au moins `NUMERO_INCLUSION_MIN_LENGTH` = **6** caractères. Ce seuil est le paramètre anti-faux-positif : un n° plus court n’est jamais considéré inclus.

Point d’entrée demande NV1 : `DepotFactureController::valider`, appel `rejeterSiDoublonProbable` à `app/Http/Controllers/DepotFactureController.php:2586`, après la complétion des montants et avant `DB::transaction`.

Autres envois qui contournent cette demande et sont aussi bloqués : `validerEtComptabiliser` (classique ligne 2949, électronique ligne 3009), `validerDejaValide` (3064), `validerHistorique` (3318). `validerWorkflowEngage` n’est pas cette demande : la compta et le paiement bloquent encore ensuite.

Enregistrement : fin de `applyDepotSaisieFromValidated` (`synchroniser`). OCR et Outlook passent le fournisseur au détecteur. Un contrôle humain dont l’empreinte couvre encore le rapprochement n’est pas réécrit et ne bloque pas.

Comptabilisation : `ComptabilisationController::assertDepotPretPourEnvoiApbill` (garde en tête, avant le contrôle « déjà un bill » inchangé) et `comptabiliser` (clôture sans envoi). Le contrôle strict « même n° normalisé + même fournisseur + même programme + bill déjà créé » est conservé : il empêche un second bill quand les n° sont strictement les mêmes. Il ne matche pas 0938M26 / 0938M26SIT4, donc la case débloque vraiment cette paire.

Paiement — tous les points d’entrée :

- Mise en paiement, tous modes (virement, chèque, autres) : `PaiementController::traiter` → `PaiementAppymtBatchService::traiterSelection` → `PaiementAppymtExecutionService::executerLigne` (garde en tête). Le chèque n’a pas d’endpoint séparé : c’est le mode dans cette méthode.
- Comptabiliser puis payer : `ComptabilisationController::confirmerPaiementApresCompta` → `traiterDepotCompta` → le même `executerLigne`.
- Virement exécuté : `FactureController::virementExecute` et `FactureGlobaleController::virementExecuteGroupe`, tous deux via `FactureVirementExecuteService::executer`.
- Confirmation du paiement effectif : `FactureController::confirmerPaiementEffectif`.

Message : n°, montant, statut, payée ou non, lien. Réponse JSON `doublon` (message, href, numero, montant, statut, payee). L’alerte de saisie et le rapport de paiement affichent le lien. Le bandeau Comparer / Voir est inchangé.

Factures électroniques : `FactureElectroniqueImport::marquerDoublons` appelle `DepotFactureDuplicateDetector` (dépôts seulement, le schéma d’import n’a pas les colonnes facture). « L’électronique prime » : un dépôt classique non comptabilisé est marqué doublon de la facture électronique. Si le classique a déjà un `intacct_bill_id`, il n’est pas re-flaggé ; c’est l’électronique qui est marquée doublon, avec le signal `doublon_comptabilise`. Un contrôle humain actif sur le classique n’est pas écrasé.

Exclus : dépôt courant, dépôts `rejete`, factures `rejetee`.

Requêtes d’un recalcul unitaire : 0 SELECT si ni fournisseur ni montant (score 3 impossible) ; sinon 2 SELECT (dépôts puis factures) filtrés par `fournisseur_id` OU `montant_ttc` ± 0,01, puis 1 UPDATE de `doublon_detecte` et `doublon_de_id` seulement si ces colonnes changent (`DB::table`, pas de `save` modèle : le statut et le paiement ne bougent pas). Index `depot_factures_montant_ttc_index` et `factures_montant_ttc_index` — migration `2026_10_16_150000_add_montant_ttc_index_for_doublons`.

Commande `factures:doublons-recalcul` : aperçu sans écriture, paires score ≥ 3, priorité « une payée / l’autre non ». `--appliquer` écrit uniquement les deux colonnes, ignore une paire déjà couverte par « ce n’est pas un doublon ». Payée = statut dépôt ou facture liée dans paye / payee / paiement_confirme. Un bill n’est pas traité comme un paiement.

## DÉPLOIEMENT / TEST

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

Pas de `route:clear` (aucune route nouvelle ; la commande est auto-découverte). Ne pas lancer `npm` ni `composer` sur le serveur.

Tests : 29 passed (détecteur, 1214/2469, saisie, NV1, compta, virement, chèque, confirmation, commande, FE y compris suffixe SIT). `journal:controler` conforme. Bundle saisie `DepotFactureSaisieForm-ydF8tcY7.js`, `grep -c cie_effacement_confirme` = 1. Paiement `Index-CpO9dQqa2.js`.

## LEÇON

Le flag était un instantané. Le recalcul doit réutiliser les matches déjà chargés pour l’empreinte (pas un troisième SELECT). Ne pas élargir le contrôle « déjà un bill » à `numerosConcordent` : la case ne débloquerait plus la compta des n° partiels déjà billés. Le seuil d’inclusion 6 est nommé `NUMERO_INCLUSION_MIN_LENGTH`. L’écriture des drapeaux passe par le query builder, jamais par `save()`, pour qu’un dépôt payé ne recule pas.
