---
suivi: 1604
date: 2026-09-17
sujet: Suppression dépôt par valideur + paiement manuel facture historique
chantier: comptabilite / depot-factures
type: feature
statut: poussé
hash: 8ecec4f3
fichiers:
  - app/Services/FactureSuppressionService.php
  - app/Http/Controllers/DepotFactureController.php
  - resources/js/Pages/DepotFactures/Show.vue
  - database/migrations/2026_09_17_190000_add_paiement_manuel_to_factures_table.php
  - app/Services/FacturePaiementManuelHistoriqueService.php
  - app/Http/Controllers/FacturePaiementManuelHistoriqueController.php
  - app/Services/FacturePaiementStatutSyncService.php
  - app/Models/Facture.php
  - app/Http/Controllers/FactureController.php
  - app/Support/FactureHistoriquePresenter.php
  - resources/js/Pages/Factures/Show.vue
  - routes/web.php
  - tests/Unit/FactureSuppressionValidateur1604SourceTest.php
  - tests/Unit/FacturePaiementManuelHistorique1604SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1604_suppression_validateur_et_paiement_historique.md
---

# SUIVI #1604 — suppression par valideur + paiement historique

## PROMPT ENVOYÉ

A. Un valideur désigné peut mettre à la corbeille un dépôt qui
l’attend, uniquement tant que la facture est au dépôt (avant
engagement workflow). Droits existants conservés. abort_unless.
B. GS_COMPTABILITE peut marquer une facture historique comme payée
(date réelle, confirmation, réversible, tracé). Sync Sage ne doit
pas écraser. Ne pas retoucher #1571.

## FICHES LUES

- **#1418** — `DepotFacture::statutsStadeDepot()` = stade « en dépôt »
  avant engagement workflow. Impact : frontière A retenue telle
  quelle (depose, attribue, en_analyse, analyse, en_cours_saisie).
  `en_validation_nv1` est le premier statut hors stade.
- **#1570 LEÇON** — huit chemins « payée » ; `paiement_effectif` NULL
  = sync auto. Impact : le marquage B pose un drapeau dédié, ne
  passe pas par `etat_paiement_sage = paye`.
- **#1571 LEÇON** — WHENPAID n’est pas un paiement ; couper
  `estApBillPaye` avant WHENPAID. Impact : **aucun** changement de
  cette règle. Anti-écrasement = `estEligible` faux si
  `paiement_manuel_at` renseigné.
- **#1527** — saisie historique éditable hors Sage,
  `SaisieHistoriqueEdition`, `saisie_historique` colonne. Impact :
  B s’appuie sur cette colonne, pas sur le JSON seul.
- **#1589 LEÇON** — corbeille = SoftDeletes + qui/quand, pas
  d’effacement. Impact : A réutilise `FactureSuppressionService`
  (déjà `deleted_at` / `deleted_by_user_id`).
- **#1598 LEÇON** — php -l / Reflection ne voient pas une variable
  d’une branche. Impact : revue des closures `marquer` / `annuler`.

## EXISTANT ÉTABLI (avant code)

### Désignation du valideur

Pas de colonne `validateur_id` sur `depot_factures`. La désignation
vit dans `ocr_raw.saisie.validateur_id` et `validateur_email`
(`FactureValidateurNv1AssignmentService::resolveValidateurDepuisSaisie`,
email d’abord puis id ERP / azure_id). `DepotFactureController::canValiderHistoriquePaye`
et `utilisateurEstAttributaire` lisent les mêmes clés JSON.

### Qui peut supprimer aujourd’hui

`FactureSuppressionService::userPeutSupprimerUnitaire` : déposant
(`depot.user_id`) **ou** `isAdminERP()` **ou**
`hasAnyM365Group(['GS_COMPTABILITE','GS_DIRECTION_GENERALE'])`.
Masse : GS_DSI / GS_ADMIN_ERP. `destroy` ne faisait **pas**
`abort_unless` 403 (retour `withErrors`). Ces droits sont
**conservés**. Le cas valideur est **ajouté** et plus étroit.

### Frontière de statut (A)

`FactureSuppressionPolicy` bloque comptabilisee / valide / payee /
Sage, mais **autorise encore** `en_validation_nv1` pour déposant /
compta. Pour le **valideur**, la frontière est plus stricte :
`DepotFacture::statutsStadeDepot()` **et** facture absente ou hors
`en_validation_nv1` / nv2 / a_comptabiliser / comptabilisee /
en_paiement / payee / rejetee, **et** aucune ligne
`workflow_validations` nv1/nv2. Dès `en_validation_nv1`, POST → 403
pour un valideur sans autre droit.

### Mécanisme : corbeille, pas suppression sèche

Déjà SoftDeletes (`deleted_at`, `deleted_by_user_id`,
`deleted_reason`) + `FactureHistorique` `supprime_logique`. Le
dialogue Show.vue disait à tort « définitivement / irréversible » :
aligné sur la corbeille, fichier + déposant nommés. Fichier physique
non effacé.

### `saisie_historique`

Colonne `depot_factures.saisie_historique` (migration 2026_05_14),
aussi dans JSON `ocr_raw.saisie`. Posée à l’upload / saisie.
`type_source=historique`. `ComptabilisationController` abort_if
historique (pas d’Intacct). `validerHistorique` crée une facture
déjà `payee` avec date de règlement. Après NV1,
`FactureWorkflowApresNv1Service` bascule historique en `payee` sans
Sage. Partie B : factures de reprise déjà en fiche, restées en
attente de confirmation Sage qui n’arrivera jamais.

### Paiement aujourd’hui

Huit chemins (#1570). Confirmation Sage via
`FacturePaiementStatutSyncService`. `validerHistorique` (saisie)
marque payée à la création — pas d’action équivalente sur la fiche.
Pas d’autre marquage manuel fiche réservé à un profil pour
l’historique.

## GARANTIE ANTI-ÉCRASEMENT SAGE

1. Colonnes `factures.paiement_manuel_at` / `_par_user_id` /
   `_date` / `_contexte` (migration justifiée : rien n’enregistrait
   un paiement hors Sage distinct de `etat_paiement_sage`).
2. `FacturePaiementStatutSyncService::estEligible` retourne `false`
   dès que `paiement_manuel_at` est non null — **avant** le reste,
   sans toucher `estApBillPaye`.
3. Le batch `syncBatchEligible` ajoute `whereNull('paiement_manuel_at')`.
4. `appliquerEtat` no-op si le drapeau est posé (défense).
5. Le marquage manuel **ne pose pas** `etat_paiement_sage = paye`,
   pour garder la distinction « Payée (Sage) » / « Payée (saisie
   manuelle) » sur la fiche.

## MAPPING

| Libellé affiché | Valeur postée | Colonne / usage back | Libellé back |
|---|---|---|---|
| Date de paiement réelle | `date_paiement` (Y-m-d) | `factures.paiement_manuel_date` | Date de paiement saisie manuellement |
| Payée (Sage) | origine `sage` (affichage, non postée) | `etat_paiement_sage = paye` | Paiement confirmé Sage |
| Payée (saisie manuelle) | origine `manuel` (affichage, non postée) | `paiement_manuel_at` renseigné | Paiement marqué manuellement |

Pas de select / toggle métier ajouté. Confirmation = dialogue +
bouton, date obligatoire sans défaut « aujourd’hui ».

## SYNTHÈSE

Valideur désigné (JSON saisie) : corbeille au stade dépôt seulement,
`abort_unless` 403. GS_COMPTABILITE : marquer / annuler un paiement
manuel sur facture historique, tracé, sync Sage ignorée.

## DÉPLOIEMENT-TEST

1. `git pull origin main`
2. `php artisan migrate --force`
3. `php artisan optimize:clear` + `view:clear` + `route:clear` + `journal:sync`
4. Tests manuels 1–10 (voir livrable). Compte Robin (GS_ADMIN_ERP)
   ne prouve pas un 403.
5. `grep "production.ERROR" storage/logs/laravel.log | tail -20`

JAMAIS npm/composer/optimize/config:cache sur OVH.

## LEÇON

Valideur = JSON `ocr_raw.saisie`, pas une colonne. Frontière A =
`statutsStadeDepot()`, pas `FactureSuppressionPolicy` (trop large
pour le valideur). Paiement manuel = drapeau dédié, pas
`etat_paiement_sage`, sinon la sync #1571 et l’affichage se
confondent. `Gate::before` / compte Robin ne prouvent aucun refus.
