# Rapport v1.7.0 — Les pièces de la chaîne se parlent

Après ses premières semaines d'utilisation réelle, l'exploitant a rendu son
verdict : échéanciers, devis, factures, primes et opérations ne se parlaient
pas assez. Quand il encaisse à un endroit, il veut voir « payé » partout,
tout de suite ; quand un devis est accepté, il veut que la suite se propose ;
quand un encaissement mérite une prime, il veut que l'application le lui
rappelle. Les principes de la maison n'ont pas bougé : **le logiciel propose,
pré-remplit intelligemment, mais ne tranche jamais en silence.**

**Recette** : 855 tests, 4 300 assertions, tout au vert (829 avant le lot).
`./vendor/bin/pint` passé, `npm run build` relancé. **Aucune migration**,
aucun changement de cron, aucune dépendance ajoutée : le déploiement est un
simple remplacement de fichiers suivi des quatre `artisan *:cache`.

---

## 1. L'écran dit vrai tout de suite

Le fait mesuré : les caches de 60 s du tableau de bord (`soldes`, `dettes`,
`dossiers incomplets`) ne s'oubliaient jamais à l'écriture. Une opération
saisie n'apparaissait dans « Position du jour » qu'au bout d'une minute,
quand « Encaissé ce mois-ci », non caché, bougeait immédiatement — deux
chiffres qui se contredisaient, et l'exploitant avait raison de le dire.

- **Toute écriture qui change l'argent oublie les caches**, branchée en un
  seul endroit (`TableauDeBord::brancherLInvalidation()`, appelé par
  `AppServiceProvider`) : opérations du journal (création, modification,
  corbeille, restauration — ce qui couvre les mouvements de comptes courants
  d'associés et les remboursements Forsa, qui passent tous par le journal),
  et encaissements de domiciliation en propre.
- **La clé par utilisateur des dossiers incomplets devient invalidable** :
  un sel de version dans la clé, incrémenté par `oublier()` ;
  `CompletudeDossiers::oublier()` propage désormais l'oubli jusqu'au
  compteur en cache — il ne vidait que son mémo objet, et le badge restait
  faux 60 secondes après la complétion d'un dossier.
- Le TTL de 60 s reste, en filet de sécurité.
- **Action d'en-tête « Actualiser »** sur le tableau de bord (page
  `App\Filament\Pages\TableauDeBord`, même route `dashboard` qu'avant) :
  oublie toutes les clés, notifie « Tableau de bord actualisé », recharge.
- Les bornes de requêtes de `RequetesDuTableauDeBordTest` n'ont pas bougé
  (< 60 et < 80). Un détail de mesure a dû suivre : le jeu d'essai ajouté en
  cours de test invalide désormais les plats — un rendu de chauffe rétablit
  la symétrie avant la comparaison, qui garde tout son sens.

## 2. Le devis accepté propose la suite

- **La notification d'acceptation porte deux boutons** — « Transformer en
  contrat » et « Établir la facture » — qui rouvrent la liste des devis avec
  la fenêtre de l'action déjà ouverte sur ce devis (mécanisme d'URL de
  Filament, aucune logique dupliquée).
- **Sur la ligne d'un devis accepté, les deux transformations sont des
  boutons visibles**, plus seulement des entrées du menu « … ». Même
  fabrique d'action en deux tenues : bouton franc sur l'accepté, entrée de
  menu pour les autres devis transformables.
- **Un devis refusé peut être ré-accepté** — l'exploitant change d'avis ;
  un devis transformé reste intouchable (policy inchangée, prouvée par
  tests).
- **`QuoteService::expirerEchus()` n'est plus du code mort** : branché dans
  `mobdie:planificateur-quotidien`, les devis envoyés dont la validité est
  dépassée passent à « Expiré » chaque matin — et restent transformables.
- **« Dupliquer »** sur chaque devis : même client, mêmes lignes, nouveau
  numéro, statut brouillon, date du jour (`QuoteService::dupliquer()`).

## 3. La facture et l'échéancier ne se contredisent plus

- **Facture groupée annulée** : `InvoiceService::annuler()` détache
  désormais *toutes* les échéances couvertes (`echeancesCouvertes()`), pas
  seulement le lien unitaire. Elles redeviennent facturables au lieu de se
  croire facturées au nom d'une pièce morte.
- **La contrainte sur `domiciliation_installments.invoice_id` existait
  déjà** : posée en v1.6.1 par la migration `poser_les_index_qui_manquent`
  (nettoyage des orphelins puis `nullOnDelete`), exactement ce que le lot
  demandait. Aucune nouvelle migration ; deux tests la prouvent désormais
  (identifiant orphelin refusé en base, échéance détachée à l'effacement
  définitif d'un brouillon).
- **Le journal ne se contourne pas** : supprimer directement du journal
  l'écriture d'un encaissement de domiciliation (tant que le paiement
  existe) ou d'un versement de prime (tant que des primes versées s'y
  rattachent) est refusé, au niveau du modèle — donc par tous les chemins —
  avec le bon geste en face du refus : « supprimez l'encaissement depuis la
  fiche client ou l'échéancier, il défait tout proprement ». La suppression
  du paiement, elle, continue de tout défaire (le paiement marque son
  écriture avant de la supprimer). Les actions du journal, unitaires et en
  lot, montrent le message au lieu d'une erreur brute.
- Le « payé à un endroit = payé partout » est prouvé dans les deux sens par
  les tests : encaisser depuis la facture solde les échéances ; encaisser
  depuis l'échéancier solde la facture quand tout est couvert (et pas
  avant) ; supprimer l'encaissement rouvre tout.

## 4. La prime ne s'oublie plus — mais ne se décide jamais seule

- **Rappel à l'encaissement** : quand un encaissement part sans prime alors
  que le barème du commercial de l'échéance en suggère une
  (`CommissionService::montantSuggere()` non nul), notification immédiate —
  et posée dans la cloche — avec le bouton « Ajouter la prime » qui rouvre
  `AjouterPrimeAction` sur l'échéance, dans l'échéancier global. Branché aux
  trois portes d'encaissement (échéancier du client, facture, action
  rapide) via `ChampsPrime::rappelerSiSuggeree()`. Rien n'est créé sans
  clic.
- **Cinquième carte « Encaissements sans prime »** dans « À traiter » sur le
  tableau de bord, avec lien vers la page des commissions — la liste
  n'était visible que là-bas, personne ne la voyait. Compteur en une seule
  requête (`nombreDEncaissementsSansPrime()`).
- `mobdie:generer-commissions` reste un rattrapage manuel : non planifié,
  comme décidé.

## 5. Modifier, annuler, supprimer — factures et devis

Décision de l'exploitant (15 août 2026), qui connaît la recommandation
contraire et l'assume : **une facture émise redevient modifiable
directement.** Le cadre qui rend ça tenable :

- **Le numéro et la série sont intouchables.** Le champ du numéro est
  verrouillé pour tout le monde — y compris le super-administrateur, à qui
  la v1.6.x réservait encore la renumérotation sous audit : la décision va
  au bout, jamais de réattribution (art. 145 CGI). Aucune modification ne
  les recalcule (`prendreRang()` ne touche jamais un rang déjà pris). La
  permission `factures.numeroter` subsiste en base mais ne commande plus
  rien.
- **Chaque modification se lit sur la pièce** : section « Historique des
  modifications » sur la facture (date, utilisateur, champ, avant → après),
  servie par le journal d'audit que le trait `Auditable` tenait déjà —
  aucune table nouvelle. Libellés français, statuts en clair, montants en
  dirhams. Rien sur le PDF client.
- **Une facture payée ne se modifie pas telle quelle** : le bouton
  « Modifier » y ouvre le flux guidé — annuler le règlement (les paiements
  se suppriment et défont tout : opération, échéance, primes non versées),
  modifier, ré-encaisser (`InvoiceService::annulerLeReglement()`). Trois
  gestes guidés, zéro incohérence de journal.
- **L'annulation s'étend aux factures payées** par le même flux : le
  règlement se défait d'abord, puis la facture s'annule — elle garde son
  numéro, comme toujours.
- **La suppression ne change pas** : brouillons et devis seulement, le
  garde-fou `FacturePriseEnSerieException` reste, le message oriente vers
  l'annulation.
- **Le PDF d'une facture modifiée se régénère avec les nouvelles valeurs** :
  il se rend depuis le modèle à chaque demande, rien n'est figé — un test
  le prouve en lisant le HTML envoyé à DomPDF.
- **Devis** : modification et suppression libres sauf transformé (policy
  existante, désormais prouvée par tests) ; « Dupliquer » ajouté (point 2).

---

## Ce que le déploiement doit savoir

- **Aucune migration.** Aucun changement de `.env`, de cron ou de
  dépendance. Paquet = fichiers + `public/build`, puis `optimize:clear`,
  `config:cache`, `route:cache`, `view:cache`.
- **Aucun fichier supprimé ni déplacé** : pas de `rm` à prévoir.
- Fichiers nouveaux notables : `app/Filament/Pages/TableauDeBord.php`,
  `app/Domaines/Facturation/Filament/Resources/Invoices/Schemas/HistoriqueDesModifications.php`,
  `resources/views/filament/factures/historique-modifications.blade.php`.
- **À vérifier à l'écran après mise en ligne** : saisir une opération →
  « Position du jour » bouge sans attendre ; marquer un devis accepté → la
  notification porte deux boutons ; encaisser sans prime un client dont le
  commercial a un barème → le rappel sonne ; ouvrir une facture émise → le
  numéro est grisé et l'historique apparaît après une modification ;
  tenter de supprimer du journal l'écriture d'un encaissement → le refus
  explique le bon geste.
