---
suivi: 1675
date: 2026-09-24
sujet: Relecture Sage à la demande sur une facture payée
chantier: factures / paiement / sage
type: fix
statut: poussé
hash: 5f9abea2
fichiers:
  - app/Services/FacturePaiementStatutSyncService.php
  - app/Http/Controllers/FactureController.php
  - app/Http/Middleware/HandleInertiaRequests.php
  - resources/js/Pages/Factures/Show.vue
  - public/build/manifest.json
  - public/build/assets/Show-ClFibHtP.js
  - tests/Feature/FacturePaiementStatutSyncTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - docs/suivi/SUIVI_1675_relecture_sage_a_la_demande.md
---

# SUIVI #1675 — relecture Sage à la demande

Remplace le périmètre du #1673 (phase 1 seulement, trois options). Robin a
tranché : relire à la demande, sur une facture, et mettre à jour le statut
si le paiement Sage est annulé. Pas de balayage, pas de recul en masse, pas
de modification du bouton global.

FAC-2026-935 a déjà été remise en `comptabilisee` par la commande #1672.
Elle ne sert plus de cas de recette pour la partie B.

## PROMPT ENVOYÉ

Bouton « Actualiser statut Sage » : relire même une facture `payee`+`paye`,
ignorer le TTL, refuser `paiement_manuel_at`. Si le header APPYMT est dans
`ETATS_APPYMT_ANNULES`, confirmation puis les sept écritures du #1672, plus
dépôt et workflow. Silence ≠ annulation. Extourne APBILL signalée, non
traitée comme une annulation de paiement. Masse inchangée.

## BASE GIT

`origin/main` = `055868b9` (#1672) au départ et au rebase : HEAD déjà égal,
rien à rejouer. #1667 n'avait pas encore touché le journal sur `origin/main`.

Fiches : #1571 (anti-régression laissée), #1604 (`paiement_manuel_at`),
#1671 (bouton inerte sur payée, file exige paiement_effectif en_attente),
#1672 (liste V/VOID/VOIDED, traces conservées), #1673 (options, non codé).

## COMPORTEMENT

`estEligible` est inchangé (lignes 33-57) : le couple `payee`+`paye` et
`paiement_manuel_at` restent exclus de la synchro automatique et du bouton
de masse (`FacturePaiementStatutBatchService` 197-207, `paye` absent de la
liste).

Le bouton appelle `relectureDemandee`, pas `syncFacture`. Cette méthode
ignore le couple payée et le TTL. Elle refuse `paiement_manuel_at` avant
toute lecture.

« Sage dit annulé » : chaque bill lu a un header pour lequel
`EtatPaiementSage::estAppymtAnnule` est vrai (`V`, `VOID`, `VOIDED`).
Ce n'est pas `etat_erp === null`. Un signal vide, un timeout, un bill sans
WHENPAID restent un silence : `appliquerEtat` n'est pas contourné, le
statut n'est pas reculé.

« Sage confirme » et la facture est déjà `payee`+`paye` : seul
`intacct_paiement_synced_at` bouge, message « Sage confirme le paiement ».
Si la facture n'est pas encore payée, le chemin habituel `appliquerEtat`
peut encore avancer.

APBILL `Reversed` / `Reversal` / `Extourné` / `Extourne` : message
explicite, statut non modifié. Ce n'est pas traité comme une annulation de
paiement. Les deux listes restent séparées.

Confirmation : premier clic = lecture seule du statut (la trace
`intacct_appymt_state` est rafraîchie, comme la synchro le faisait déjà).
Dialogue : message + les sept champs. Second clic `confirmer_annulation`
relit Sage et n'écrit que si le verdict est encore l'annulation.

Écritures confirmées : statut `comptabilisee`, `etat_paiement_sage` NULL,
`intacct_when_paid` NULL, `intacct_appymt_when_created` NULL, dépôt
`comptabilisee`, `date_paiement` NULL, `paiement_effectif` valide →
`en_attente`. Ids Sage et lignes `paiements_intacct` conservés. Historique
`relecture_sage_annulation_appymt` via `logHistorique`, avec ce que Sage a
répondu.

Aucun appel Sage en écriture. `paidBillIdsLookup` non réécrit. Outil de
forçage non touché.

## DROITS

Route `routes/web.php` 511-513 :

    middleware m365.groups:GS_COMPTABILITE,GS_DIRECTION_GENERALE,GS_DAF,GS_DSI,GS_ADMIN_ERP

Le bouton n'est pas ouvert à tout visiteur de la fiche. Le même filtre est
sur `can_actualiser_statut_sage`. Périmètre déjà adapté à un geste de
paiement : comptabilité, direction, DAF, DSI, admin ERP. Non resserré.
Compte tiers pour le 403 : un utilisateur dont le seul groupe est
`GS_HECTARE` ou `GS_ENVOL`, sans aucun des cinq groupes ci-dessus.
`Gate::before` vrai pour GS_ADMIN_ERP ne prouve pas ce refus.

## CLOSURES

`appliquerAnnulationAppymt` : `use ($facture, $depot, $changements, $sage)`
— les quatre sont lues dans la transaction. `classerRelecture` et
`relectureDemandee` n'ajoutent pas de closure.

## VÉRIFICATION

`php -l` : No syntax errors detected sur les PHP modifiés.
ReflectionClass + make : FactureController, FacturePaiementStatutSyncService,
FacturePaiementStatutBatchService, IntacctPaiementStatusService.
Bundle fiche : `Show-ClFibHtP.js`.
File de paiement et bilan : non mesurés (pas d'essai sur une facture payée
en base locale). FAC-2026-935 n'est plus le cas. La recette se fait sur une
facture encore `payee` dont l'APPYMT Sage est annulé. Le dépôt qui quitte
`paye` sort de « Facturé payé » : variation attendue, limitée à cette facture.

## DÉPLOIEMENT

git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan journal:sync

Pas de migrate. Pas de route:clear (route existante). Jamais npm, composer,
optimize, config:cache sur OVH.

## LEÇON

`etat_erp === null` mélange silence, extourne de bill et APPYMT voidé. Seul
`estAppymtAnnule` sur le header retenu autorise le retour en file, et
seulement après confirmation. Le couple payée ne se relit pas tout seul.
