---
suivi: 1457
date: 2026-09-04
sujet: Diagnostic — tâche opérationnelle valideur en amont de NV1
chantier: depot_factures
type: diagnostic
statut: poussé
hash: ae3dc7bd
fichiers:
  - docs/suivi/SUIVI_1457_diag_tache_valideur_amont.md
---

# SUIVI #1457 — Diagnostic tâche valideur amont (PHASE 1)

## PROMPT ENVOYÉ

Cartographier le mécanisme des tâches NV1 pour cloner le pattern au #1458
(tâche « Compléter et valider une facture » au valideur désigné, immédiat,
clôture à l’envoi). Lecture seule. Aucun chiffre inventé. SQL pour Robin.

## FICHES LUES

- **#1450** — `ocr_raw.saisie.validateur_id` (pas de colonne) ; enregistrement
  permissif vs envoi strict ; défaut valideur = déposant.
- **#1453** — `depot-factures:backfill-validateur` `{--apply}` ; JSON_SET
  `$.saisie.validateur_id` ; statuts boîte y compris `analyse`.
- **#1454** — HelpHint filtres boîte ; hors périmètre tâches.
- **#1394** (cité) — `attach()`/`sync()` sur pivot **taches_suivi** : autre
  monde. Les tâches NV1 n’utilisent **pas** ce pivot.

Base : `origin/main` `28799deb` (chore #1456). Attendu prompt `55f38e37`
(#1455) : **main a avancé** (#1456 juridique aperçu). Diagnostic lu sur ce main.

MySQL local : non utilisé. Aucune requête de volume exécutée. Aucun chiffre.

## CLAUSE D’ARRÊT

**Non déclenchée.** Les tâches NV1 sont créées **dans l’ERP** par
`TaskSyncService` + `ValidationFactureNv1WorkflowHooks`. Pas de Power Automate.

**Distinction critique :** ce ne sont **pas** des `TacheSuivi` (`taches_suivi`).
Ce sont des `Task` (`tasks`) — « tâches opérationnelles AUTO ». Cloner NV1 =
cloner **ce** moteur, pas Mes tâches suivi.

---

## A. CRÉATION DES TÂCHES NV1

### A1. Fichier / méthode au passage `en_validation_nv1`

Chaîne mesurée :

1. `DepotFactureController::valider` (`app/Http/Controllers/DepotFactureController.php`
   L1899) — envoi. Crée `Facture` + `WorkflowValidation` nv1 `en_attente`
   (L2157+) et pose `depot_factures.statut = en_validation_nv1` (L2140–2144)
   **si** le déposant n’est pas le valideur désigné.
2. En fin de `valider`, si `! $estValidateurDesigne` :
   `NotificationFactureService::notifierValidateurNv1` (contrôleur L2342).
3. `NotificationFactureService::notifierValidateurNv1`
   (`app/Services/NotificationFactureService.php` L20–90) charge le workflow
   nv1 `en_attente` puis :

   `ValidationFactureNv1WorkflowHooks::upsertAfterNv1Entry($workflow)` L88–89.

Autres points d’entrée du **même** hook (facture déjà engagée) :

- `DepotFactureEngagerWorkflowService` L191–192 (déjà réglée / engagement).
- `FactureValidateurNv1AssignmentService::assign` L59–60 et L113
  (réassignation / upsert).
- `FactureController` création manuelle L357 (`notifierValidateurNv1`).

Le hook lui-même (`ValidationFactureNv1WorkflowHooks.php` L18–27) :

- refuse si `niveau !== 'nv1'` ou `statut !== 'en_attente'` ;
- appelle `ValidationFactureNv1TaskSync::upsertForWorkflow` ;
- catch Throwable → `Log::warning`, **le workflow continue**.

`upsertForWorkflow` (`ValidationFactureNv1TaskSync.php` L21–35) appelle
`TaskSyncService::upsertFromEvent(TaskType::CODE_VALIDATION_FACTURE_NV1, $workflow, $assignee, $context, $titre)`.

**Le taskable n’est pas le dépôt : c’est `WorkflowValidation`.**

### A2. Modèle / table / colonnes

Classe `App\Models\Task`, table `tasks`.

Migration créatrice `database/migrations/2026_06_12_210100_create_tasks_table.php`
L11–37 : `id`, `uuid`, `task_type_id` (FK `task_types`), `titre`, `description`,
`priorite`, `statut`, `assignee_id` (FK users nullable), `assignee_email`,
`assignee_azure_id`, `taskable_type`, `taskable_id`, `erp_context` (json),
`echeance_at`, `echeance_source`, `auto_closed`, `closed_reason`, `closed_at`,
`closed_by_user_id`, `created_by_user_id`, `idempotency_key` (unique), timestamps.

Ajout ultérieur : `assignee_groupe_m365`
(`database/migrations/2026_06_22_100000_add_assignee_groupe_m365_to_tasks_table.php`
L20).

Types : table `task_types`
(`database/migrations/2026_06_12_210000_create_task_types_table.php` L11–21) :
`id`, `code` unique, `libelle`, `delai_heures`, `taskable_type`, `auto_create`,
`auto_close_on_erp_action`, `actif`, `config`, timestamps.

### A3. Type de tâche

Colonne `task_types.code` (varchar) + constantes `TaskType` + seed `updateOrCreate`.
**Pas d’enum PHP métier unique en base** : `TaskSyncService` refuse un code
absent ou `actif = false` (L86–89).

Constantes `app/Models/TaskType.php` L14–35 et libellés seed :

| code | libellé seed | migration seed |
|------|----------------|----------------|
| `validation_facture_nv1` | Validation Facture N1 | `2026_06_12_220000_seed_validation_facture_nv1_task_type.php` L16–19 (`delai_heures` 48) |
| `validation_facture_nv2` | Validation Facture N2 | `2026_06_26_100000_…` |
| `virement_a_executer` | Virement à exécuter | `2026_06_26_100100_…` |
| `signalement_dsi` | Signalement à traiter | `2026_06_22_100100_…` (`delai_heures` null) |
| `arbitrage_cs_fonciere` | Arbitrage condition suspensive foncière | `2026_07_30_190200_…` |
| `promesse_expiration_fonciere` | Décision — expiration de promesse foncière | `2026_07_31_120000_add_sortie_to_foncier_alerte_configs.php` |
| `dossier_veille_revue_fonciere` | Décision — revue de dossier en veille | idem |
| `offre_expiration_fonciere` | Décision — expiration d’offre foncière | idem |
| `dossier_sans_echange_fonciere` | Décision — dossier sans échange | idem |

Titre **instance** NV1 (pas le libellé type) : `Validation NV1 - {référence}`
(`ValidationFactureNv1TaskSync.php` L87–97).

Front rail Mes tâches (`resources/js/utils/tacheSuiviLabels.js` L226–233) —
mapping **fermé** (contenu intégral) :

- `validation_facture_nv1` → `VALID. N1`
- `validation_facture_nv2` → `VALID. N2`
- `virement_a_executer` → `VIREMENT`
- `signalement_dsi` → `SIGNAL.`
- **tout autre code** → `OPÉR.` (fallback, L233)

Preuve peuplée : le mapping est dans le source commité, pas un tableau vide.
Un nouveau code **apparaît** dans la liste (pas de whereIn types sur l’index
Mes tâches) mais le rail restera `OPÉR.` tant que ce fichier n’est pas étendu.

### A4. Destinataire

**Pas de pivot.** Colonnes `tasks.assignee_id` / `assignee_email` /
`assignee_azure_id`. Assignation NV1 = `User` résolu puis `assignee_id = $user->id`
dans `TaskSyncService::upsertFromEvent` L113–115.

Réassignation : `TaskSyncService::reassign` L278–289 (`forceFill` des trois
colonnes). Appelé par `ValidationFactureNv1TaskSync::reassignForWorkflow` L59–63
puis `reassignAfterNv1Change` (hooks L41–51). **Pas** `attach()`/`sync()`.

Le #1394 (pivot `tache_suivi_personnes`) **ne s’applique pas** ici.

Résolution assignee NV1 (`ValidationFactureNv1TaskSync.php` L66–84) :
`FactureValidateurNv1AssignmentService::resolveReferentDesignePourListe` →
`users.id` entier, sinon `workflow.validateur_id`.

### A5. Échéance

Oui. `TaskSyncService::calculateEcheance` L292–298 :
`now() + task_types.delai_heures` si non null. NV1 seed = **48 heures**.
`echeance_source = type`. Posée **uniquement à la création** (L123–124), pas
recalculée au updateOrCreate.

« Immédiat / pas de carence » (cible #1458) ≠ absence d’échéance : NV1 est
créée tout de suite **avec** une échéance à +48 h. À arbitrer pour le nouveau
type : même 48 h, autre délai, ou `delai_heures` null (comme signalement).

### A6. Lien de retour

Stocké dans `erp_context` JSON (`ValidationFactureNv1TaskSync.php` L103–111) :
`facture_id`, `depot_uuid`, `reference`, `niveau`.

URL affichée Mes tâches : `MesTachesController::serializeTaskRow` L318–322 —
`route('factures.fiche.show', facture)` si `facture_id`, sinon bug-report,
**sinon `null`**. Le `depot_uuid` du contexte **n’est pas** utilisé pour
`fiche_url`.

La boîte dépôt s’ouvre en `/depot-factures?depot={uuid}` (`Index.vue` L76–77),
pas `/depot-factures/{uuid}` (Show).

**Impact #1458 :** une tâche amont **sans** `facture_id` aurait `fiche_url`
null **sauf** si `serializeTaskRow` (et éventuellement le front) est étendu
pour `erp_context.depot_uuid` → `/depot-factures?depot=…`.

---

## B. CLÔTURE DES TÂCHES NV1

### B1. Validation donnée

Pas de delete. `TaskSyncService::closeTask` → `applyClosureToTask` :
`statut = realise`, `auto_closed = true`, `closed_reason`, `closed_at`,
`closed_by_user_id` (`TaskSyncService.php` L269–275).

Raison `Task::CLOSED_REASON_VALIDATION_ERP` (`validation_erp`) → `realise`
(L247).

Appels `closeAfterNv1Action(..., VALIDATION_ERP)` :

- `FactureValidationService.php` L152–156
- `DepotFactureValiderDejaValideService.php` L396
- `DepotFactureValiderEtComptabiliserService.php` L151

Cas particulier `reason === 'blocage'` : **ne ferme pas**, passe `en_cours`
(L214–217). Déclenché depuis `DepotFactureController::valider` L2293–2295.

Une tâche déjà `realise`/`annule` n’est **jamais ressuscitée** (`upsertFromEvent`
L100–102).

### B2. Rejet / suppression

**Rejet :** `FactureController` L1489–1493 et `ValidationController` L408–412
avec `CLOSED_REASON_REJET_ERP` → `statut = annule` (TaskSyncService L248).
Dépôt : `STATUT_REJETE` (`ValidationController` L426).

**Suppression :** `FactureSuppressionService` L426 appelle
`ValidationFactureSupprimeeHandler::closeOpenValidationTasksForFacture`
**avant** soft-delete (`#654`). Raison `facture_supprimee` → `annule`.
Couvre NV1 **et** NV2.

Dépôt soft-deleted sans facture : handler orphelin batch
`factures:nettoyer-nv1-orphelines` (filet).

### B3. Commande de réconciliation

Oui : `factures:nettoyer-nv1-orphelines` `{--apply}` `{--notify}`
(`NettoyerNv1OrphelinesCommand.php` L21–25).

Backfill création : `taches:backfill-validation-nv1` `{--apply}`.

**Ne pas lancer** `dashboard:prune-obsolete-widgets --force` (consigne Robin).

---

## C. POINT DE DÉCLENCHEMENT DE LA NOUVELLE TÂCHE

### C1. Enregistrement + écriture valideur

`DepotFactureController::update` L1758 (appel réel L1814) →
`applyDepotSaisieFromValidated` L1464 (3ᵉ arg `! $apresEngagement`).

`validateur_id` / `validateur_email` écrits dans **`ocr_raw.saisie`**
(L1637–1638), pas de colonne SQL `validateur_id` sur `depot_factures`.
Confirmé modèle + `ensureValidateurDeposantParDefaut` L3823–3848
(`azure_id` sinon `(string) users.id`).

Règle brouillon : `validateur_id` nullable string max 255 (`depotDraftSaisieValidationRules` ~L906).

### C2. Modification après coup

Oui. Même `update` / `applyDepotSaisieFromValidated` : le tableau `saisie` est
**réécrit** (L1600–1653), le valideur peut changer.

Si `facture_id` déjà posé : `syncFromDepotSaisieIfNv1Pending` L1753–1755
→ `assign(..., force: true)` → `reassignAfterNv1Change`.

**En amont (pas encore de facture) : aucun hook tâche aujourd’hui.** Le #1458
doit brancher upsert/reassign **ici**, pas seulement à l’envoi.

### C3. Valideur vidé

Oui : champs nullable. `validateur_id` peut devenir null. Aucune tâche amont
à fermer aujourd’hui. Proposition : clôturer `annule` / `cause_resolue` (ne
pas laisser une tâche ouverte orpheline). À arbitrer.

### C4. Transition « envoyé en validation »

Méthode : `DepotFactureController::valider` L1899.

Départ : dépôt **sans** `facture_id` (sinon 422 L1918–1921). Statuts boîte
typiques : `depose` / `analyse` / `attribue` / `en_cours_saisie` (pas de garde
explicite « uniquement ces statuts » sur `valider`, hors archive / historique).

Arrivée dépôt si parcours NV1 classique (`! $estValidateurDesigne`) :
`DepotFacture::STATUT_EN_VALIDATION_NV1` (L2140–2144). Facture :
`Facture::STATUT_EN_VALIDATION_NV1` (L2106).

Création `WorkflowValidation` nv1 `en_attente` L2157+.

Puis `notifierValidateurNv1` → upsert tâche NV1 (A1).

**C’est cet envoi qui doit clôturer la future tâche amont.**

Autres envois : `validerHistorique`, `validerEtComptabiliser`,
`valider-deja-valide`, `DepotFactureEngagerWorkflowService` — à traiter au
#1458 pour ne pas laisser des tâches amont ouvertes.

### C5. Règle #1450 (ne pas contourner)

Brouillon : `depotDraftSaisieValidationRules` L858+ — permissif (pas de
marché/poste/destination requis).

Envoi : `depotFactureValiderValidationRules` L958+ **plus**
`assertRattachementBudgetCompletPourEnvoi` L5048–5101 :

- FRAIS_GENERAUX : OK (catégorie déjà required_if) ;
- destination ≠ PROGRAMMES : 422 destination ;
- PROGRAMMES : `sans_marche` **ou** `marche_id` **ou** poste
  (`poste_budgetaire_type_id` / `programme_budget_id` / `rattachement_poste_direct`) ;
- sinon 422 `marche_id` (message incomptabilisable).

Appels : `valider`, `validerHistorique`, payloads déjà-validé /
valider-et-comptabiliser (fiche #1450).

La nouvelle tâche **n’autorise pas** l’envoi : elle relance le valideur pour
**compléter puis** appeler `valider` (qui reste strict).

### C6. Commande #1453

Fichier `app/Console/Commands/BackfillDepotFactureValidateurCommand.php` L17–18 :

`depot-factures:backfill-validateur`
`{--apply : Écrit ocr_raw.saisie.validateur_id (dry-run par défaut)}`

Vérifié `php artisan list --raw` sur ce main : la ligne existe.

JSON_SET uniquement `$.saisie.validateur_id` = azure_id déposant sinon
`users.id`. Statuts : `depose`, `attribue`, `en_analyse`, `analyse`,
`en_cours_saisie` (L27–35). Dry-run par défaut.

Erreur prod `There are no commands defined in the "depot" namespace` :
Laravel parse le préfixe **avant le premier `:`**. La commande réelle est
dans le namespace `depot-factures`, **pas** `depot`. Un appel
`php artisan depot:…` produit exactement ce message. Autre cause possible :
lot #1453 **non déployé** au moment de l’essai.

---

## D. CARTOGRAPHIE DES STATUTS DÉPÔT

### D1. Valeurs `depot_factures.statut` (ENUM, `DepotFacture::statutsAutorisesColonne` L76–92)

Ordre de vie **typique** (pas un state machine unique) :

| valeur | libellé boîte `Index.vue` statutLabel L240–254 | position |
|--------|-----------------------------------------------|----------|
| `depose` | Depose | **amont** (PDF posé, OCR pas OK ou pas encore) |
| `en_analyse` | En analyse... | **amont** (ENUM + listes ; **aucun `update` applicatif mesuré** qui l’écrit) |
| `analyse` | A saisir | **amont** (OCR OK, saisie à faire) |
| `attribue` | Attribue | **amont** (valideur renseigné depuis depose/analyse/en_analyse — contrôleur L1688–1696) |
| `en_cours_saisie` | En cours | **amont** (premier enregistrement saisie, `applyDepotSaisieFromValidated` 3ᵉ arg true) |
| `en_validation_nv1` | Validation NV1 | **en validation** |
| `en_validation_nv2` | Validation NV2 | **en validation** |
| `a_comptabiliser` | A comptabiliser | **aval** |
| `comptabilisee` | (pas dans statutLabel boîte ; liste globale « Comptabilisée ») | **aval** |
| `valide` | Valide | **aval** (legacy / synchro) |
| `paye` | Payee | **aval** |
| `rejete` | Rejete | hors flux (peut relancer OCR L712–717) |
| `archive` | Archivée | hors flux |

Presenter historique (`FactureHistoriquePresenter.php` L69–71) : `analyse` →
« Analyse », `en_cours_saisie` → « En cours de saisie ».

**Collision de nom :** `FactureStatutService::RAW_GROUPS['analyse']` L17
regroupe `recue|depose|attribue|en_analyse|analyse|en_cours_saisie` sous le
filtre liste globale « En analyse ». Ce n’est **pas** le seul statut SQL
`analyse`.

### D2. `analyse` — AMONT de l’envoi (sourcé)

**Produit par** `DepotFactureClaudeAnalysisService` après OCR réussi :
`'statut' => DepotFacture::STATUT_ANALYSE` (`app/Services/DepotFactureClaudeAnalysisService.php`
L105–117), via `DepotFactureController::analyserPdf` L5923–5926
(`analyzeAndPersist`).

**Consommé comme statut de boîte** : `statutsBoiteDepot` / `statutsStadeDepot`
(`DepotFacture.php` L106–129) ; filtre « à compléter » L3091–3097
(`analyse`, `en_cours_saisie`, `attribue`) ; OCR relançable L716.

**N’est pas** post-comptabilisation : les statuts aval commencent à
`en_validation_nv1` / `a_comptabiliser`. `statutsComptablesMarche` L137–146
**n’inclut pas** `analyse`.

Gisement rattrapage tâches amont = statuts **stade dépôt** avec valideur,
**pas** les 303 `analyse` seuls : y ajouter au minimum `en_cours_saisie`,
`depose`, `attribue`, et l’ENUM `en_analyse` s’il reste des lignes.

### D3. `depose` et `en_cours_saisie`

**`depose`** : création PDF `finalizeDepotAfterLocalPdfStored` L433 (sauf
saisie historique → déjà `en_cours_saisie`). OCR en échec : reste / repasse
`depose` L461–462 et L5928–5929.

**`en_cours_saisie`** : upload historique L433 ; **et** premier PATCH saisie
`applyDepotSaisieFromValidated(..., $setStatutEnCoursSaisie = true)` L1686–1687
quand le dépôt n’est pas encore engagé.

Avance vers validation : uniquement `valider*` (C4), pas un cron.

---

## E. AFFICHAGE ET DIGEST

### E1. Où NV1 apparaît

- Page **Mes tâches**, onglet opérationnelles : `MesTachesController::index`
  L75–78 — `Task::visiblePourUtilisateur` → `ouvertes()`, **aucun filtre
  de `task_types.code`**. Un nouveau type avec `assignee_id` y entre **tout seul**.
- Rail : mapping fermé `libelleRailOperationnel` (A3) — **à étendre** sinon `OPÉR.`
- `fiche_url` : aujourd’hui facture/bug seulement (A6) — **à étendre** pour le dépôt.
- Widget dashboard « Mes factures à valider »
  (`app/Services/Dashboard/WidgetDataService.php` `facturesAValider` L188–207) :
  **Facture** en `en_validation_nv1|nv2` + `workflow_validations.validateur_id`.
  **N’inclura pas** la tâche amont (pas encore de facture). Comportement cohérent.
- `DepotFactureController::suivanteAValider` L3012–3021 : **liste blanche**
  NV1+NV2 seulement. Ne pas y coller le type amont (autre geste).

### E2. Digest 9h

`TachesDigestCommand` (`taches:digest`) → `resumePourDigest` →
`queryTasksDigest` (`MesTachesResumeService.php` L268–273) :
tâches **ouvertes** avec `assignee_id = user.id`. **Pas** de liste fermée de
codes. Ventilation par `task_types.libelle` (L138–163).

Nouveau type **entre automatiquement** dans le digest **si** `assignee_id`
est un user ERP et le type est seedé avec un `libelle`. HTML (L182–228), pas markdown.

### E3. Compteur / badge

`MesTachesResumeService::resumePourUtilisateur` L36–42 :
`Task::visiblePourUtilisateur($user)->ouvertes()->count()` → nav
`AppShell.vue` (`mes_taches.operationnelles`). **Tous types.** Le nouveau
type incrémente le badge **sans** autre déclaration.

`HandleInertiaRequests::computeFacturesEnAttenteCount` L334–362 : **factures**
NV1/NV2 / PDG / compta — **pas** les `Task`. Inchangé pour l’amont.

---

## F. REQUÊTES (à coller en prod — NE PAS exécuter ici)

Statuts amont retenus (stade dépôt #1418) : `depose`, `attribue`, `en_analyse`,
`analyse`, `en_cours_saisie`.

Valideur « renseigné » : même test JSON que #1453 / filtre #1450.

### F1. Volume par statut + ancienneté

SELECT
  d.statut,
  COUNT(*) AS c,
  ROUND(AVG(DATEDIFF(NOW(), d.created_at))) AS jours_moy,
  MAX(DATEDIFF(NOW(), d.created_at)) AS jours_max
FROM depot_factures d
WHERE d.deleted_at IS NULL
  AND d.statut IN ('depose', 'attribue', 'en_analyse', 'analyse', 'en_cours_saisie')
  AND JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id') IS NOT NULL
  AND JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) <> 'null'
  AND TRIM(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))) <> ''
GROUP BY d.statut
ORDER BY c DESC;

(Variante Robin limitée à `en_cours_saisie`,`depose`,`analyse` : sous-estime
`attribue`.)

### F2. 30 plus anciens

SELECT
  d.id,
  d.uuid,
  d.statut,
  d.user_id AS deposant_id,
  JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) AS validateur_ref,
  JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_email')) AS validateur_email,
  DATEDIFF(NOW(), d.created_at) AS jours
FROM depot_factures d
WHERE d.deleted_at IS NULL
  AND d.statut IN ('depose', 'attribue', 'en_analyse', 'analyse', 'en_cours_saisie')
  AND JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id') IS NOT NULL
  AND JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) <> 'null'
  AND TRIM(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))) <> ''
ORDER BY d.created_at ASC
LIMIT 30;

### F3. Nom du valideur (UUID azure_id **ou** id ERP string)

Confirmé code : `ensureValidateurDeposantParDefaut` L3841–3842 ;
filtre `whereSaisieValidateurEstUser` L3397–3407 ;
`FactureValidateurNv1AssignmentService::resolveUser` L158–168 (digit → `users.id`,
sinon `users.azure_id`).

SELECT
  d.id,
  d.statut,
  JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) AS validateur_ref,
  u.id AS user_id,
  u.name,
  u.email,
  u.azure_id,
  CASE
    WHEN u.azure_id IS NOT NULL
      AND JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) = u.azure_id
      THEN 'azure_id'
    WHEN CAST(u.id AS CHAR) = JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))
      THEN 'users.id'
    ELSE 'non_resolu'
  END AS jointure
FROM depot_factures d
LEFT JOIN users u
  ON u.azure_id = JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))
  OR CAST(u.id AS CHAR) = JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))
WHERE d.deleted_at IS NULL
  AND d.statut IN ('depose', 'attribue', 'en_analyse', 'analyse', 'en_cours_saisie')
  AND JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id') IS NOT NULL
  AND JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id')) <> 'null'
  AND TRIM(JSON_UNQUOTE(JSON_EXTRACT(d.ocr_raw, '$.saisie.validateur_id'))) <> ''
ORDER BY d.created_at ASC
LIMIT 50;

### F4. Idempotence — type pas encore créé

SELECT id, code, libelle, actif, delai_heures
FROM task_types
WHERE code IN (
  'completer_saisie_facture',
  'completer_valider_facture',
  'validation_facture_amont'
);

SELECT COUNT(*) AS taches_hypothetiques
FROM tasks t
JOIN task_types tt ON tt.id = t.task_type_id
WHERE tt.code IN (
  'completer_saisie_facture',
  'completer_valider_facture',
  'validation_facture_amont'
);

Attendu : 0 ligne types + 0 tâches (le type n’existe pas encore).

---

## G. RISQUES ET IMPACTS

### G1. Volume / timeout

`TaskSyncService` est **synchrone** (pas de Job). `QUEUE_CONNECTION=sync` n’est
pas le goulot ici. Un backfill artisan (clone `taches:backfill-validation-nv1`)
boucle `upsertForWorkflow` en CLI (`max_execution_time` 300 s). Risque si
milliers de dépôts : paginer / `--limit`. **Pas de chiffre ici** — F1 donne
le volume.

### G2. Doublons

Clé existante : `idempotency_key = typeCode:morphClass:id`
(`TaskSyncService.php` L301–304). Pour l’amont : taskable = **`DepotFacture`**
(pas `WorkflowValidation`, inexistant avant envoi).

PATCH `update` rejoué → `updateOrCreate` sur la même clé. Tâche déjà fermée
(envoi) → **non ressuscitée** (L100–102).

### G3. Fichiers #1458 / zone chaude

Série obligatoire : `DepotFactureController` (update + valider* + éventuellement
`serialize` n/a).

Autres fichiers prévisibles :

- `app/Models/TaskType.php` (constante)
- migration seed `task_types`
- `app/Services/Tasks/CompleterSaisieFactureTaskSync.php` (nouveau, clone NV1)
- `app/Services/Tasks/CompleterSaisieFactureWorkflowHooks.php` (nouveau)
- `app/Services/TaskSyncService.php` (seulement si nouvelle `closed_reason`)
- `app/Http/Controllers/MesTachesController.php` (`serializeTaskRow` URL dépôt)
- `resources/js/utils/tacheSuiviLabels.js` (rail) → **rebuild Vite**
- commande `taches:backfill-completer-saisie` (nom à caler)
- tests source

Pas de lot parallèle connu sur `DepotFactureController` après #1450/#1454.
`MesTaches/Index.vue` est zone disputée : viser de **ne pas** la modifier si
`fiche_url` suffit.

### G4. Droits / test visibilité

Pas de Gate sur la liste opérationnelle : `Task::scopeVisiblePourUtilisateur`
= `assignee_id` **ou** groupe M365 si `assignee_id` null
(`Task.php` L218–231). NV1 assigne un **user**, pas un groupe.

Robin = seul `GS_ADMIN_ERP` : `hasAnyM365Group` / `Gate::before` inutilisables
pour un refus. Tester avec un **compte tiers** qui est le `validateur_id` du
dépôt (ex. Manon / valideur métier), **pas** Robin. L’index ops n’est **pas**
« admin voit tout » (`MesTachesResumeService.php` L22 ; `Task::scopeVisiblePourUtilisateur`
L218–231 = `assignee_id` ou groupe M365 si `assignee_id` null).

---

## PROPOSITION D’IMPLÉMENTATION #1458 (ne pas coder ici)

Ordre :

1. Seed `task_types` + constante (code proposé : `completer_saisie_facture`,
   libellé type « Compléter et valider une facture », `taskable_type` =
   `DepotFacture`, `auto_create` / `auto_close_on_erp_action` true,
   `delai_heures` à arbitrer).
2. `CompleterSaisieFactureTaskSync` clone `ValidationFactureNv1TaskSync` :
   upsert/close/reassign ; assignee via
   `FactureValidateurNv1AssignmentService::resolveUser` sur JSON saisie ;
   `erp_context.depot_uuid` ; titre = libellé Robin + n° facture si connu.
3. Hooks appelés depuis `applyDepotSaisieFromValidated` **après** persist :
   si valideur renseigné et statut stade dépôt → upsert ; si vidé → close
   `cause_resolue` (ou raison dédiée) ; si changé → reassign.
4. Clôture `validation_erp` (ou raison dédiée `envoi_validation`) dans
   `valider` / `validerHistorique` / engager / valider-et-comptabiliser /
   deja-valide **après** succès, **avant** ou **avec** l’upsert NV1 (deux
   tâches distinctes, deux clés).
5. `MesTachesController::serializeTaskRow` : si `depot_uuid` et pas de
   `facture_id` → URL `/depot-factures?depot={uuid}`.
6. Étendre `libelleRailOperationnel` (ex. `SAISIE`).
7. Commande backfill dry-run, même filtre SQL que F1, idempotency dépôt.
8. Tests source : constante, hook dans `applyDepotSaisieFromValidated`,
   close dans `valider`, mapping rail, `inline` n/a.

Ne pas créer de `TacheSuivi`. Ne pas passer par `AlerteService` seul.

---

## QUESTIONS RESTANT À ARBITRER (Robin)

1. Code `task_types` exact (`completer_saisie_facture` vs autre).
2. `delai_heures` : 48 comme NV1, autre, ou null.
3. **Effet de bord #1450 :** le valideur par défaut **est le déposant**.
   « Immédiat dès qu’un valideur est renseigné » ⇒ **chaque dépôt en boîte**
   crée une tâche pour le déposant (sauf s’il a désigné quelqu’un d’autre).
   Est-ce voulu ?
4. Valideur vidé : `annule` ou `realise` ?
5. Backfill dans le même lot #1458 ou lot suivant, après F1 ?
6. Inclure `attribue` / `en_analyse` / `depose` dans le backfill, pas seulement
   `analyse` ?

## LEÇON

NV1 = table `tasks` + `TaskSyncService` + `WorkflowValidation` comme taskable.
L’amont n’a pas encore de workflow : taskable = `DepotFacture`. Ne pas
confondre avec `taches_suivi`. `analyse` (OCR) ≠ groupe de filtre « En analyse ».
Le mapping rail Mes tâches est une **liste fermée à 4 codes** + fallback ;
le digest et le badge, eux, sont ouverts.
