# Rapport v1.15.3 — « un contrat marqué Expiré ne laisse plus de dette derrière lui »

*Base : v1.15.2 (`371b62e`). Petit lot, un commit. **Aucune migration, aucun
e-mail, aucune dépendance.** Se déploie avec v1.16.0, en un seul paquet
v1.15.2 → v1.16.0 (le guide de déploiement s'écrit en fin de v1.16.0).*

Tests : **1 480** (1 479 au vert, 8 931 assertions) ; le seul échec est
l'échec d'environnement connu `PortailSousDomaineTest` (`APP_URL` du `.env` du
Mac, signalé depuis v1.15.0).

## Sources des chiffres

- **Faits du 16/10** (production, lus par Cowork) : NOVERA (client 123,
  2026-116) et DEVANO (client 128, 2026-121), C1 « Expiré » à la main, C2 actif.
- **Copie de production du 01/10** montée dans une base jetable
  (`mobdie_v1160`). Sur la copie, NOVERA et DEVANO n'ont qu'un contrat (le C2
  est né après le 01/10) : pour obtenir la sortie attendue, l'état du 16/10 a
  été **reconstitué sur la base jetable** (C1 passé « Expiré », C2 et son
  échéance n° 1 ajoutés aux dates de production), puis la commande simulée et
  appliquée **sur la base jetable seulement**.

## 1.A — `reprise:contrat-en-double` reconnaît le contrat déjà sorti à la main

- `ContratEnDouble::chevauchements($client, avecExpires: true)` : un contrat
  **« Expiré »** qui chevauche un contrat actif, **n'a rien encaissé** et porte
  des échéances non payées forme un couple ; jamais un contrat résilié ni
  annulé, jamais deux expirés. Le contrôle 12 de `mobdie:controler-factures`,
  le diagnostic « contrat en double » et `PaiementHorsEcheancier` gardent les
  seuls contrats actifs (comportement inchangé).
- Sans options, la liste marque le contrat « — déjà marqué Expiré à la main » et
  propose de **garder l'actif, retirer l'expiré**, sans la réserve « la commande
  refusera » (elle ne refuse pas : rien n'a été encaissé).
- `planifier` : contrat gardé actif ; contrat retiré actif **ou expiré** ;
  **résilié → refus motivé** (« une résiliation a ses documents et sa date
  d'effet ») ; expiré qui ne chevauche pas → refus, renvoi au geste de la fiche ;
  expiré avec une échéance payée → refus (« on ne retire pas un contrat qui a
  encaissé »). Le geste ne change pas : échéances non payées annulées (motif
  **« Saisi deux fois — remplacé par 2026-116-C2 »** pour un expiré ; le motif
  v1.15.1 « doublon de … » reste pour deux actifs), contrat → `annule`,
  relances en attente écartées, aucun argent, aucun document, audit.

**Sortie sur la base jetable (état du 16/10 reconstitué)** :

```
php artisan reprise:contrat-en-double 123
2026-116 — NOVERA TECHNOLOGIES : contrats actifs qui se chevauchent (ou déjà marqués Expiré à la main)
| 2026-116-C1 — déjà marqué Expiré à la main | 28/08/2026 → 27/02/2027 | 600,00 DH | n° 1 28/08/2026 En retard |
| 2026-116-C2                                | 09/10/2026 → 08/04/2027 | 600,00 DH | n° 1 09/10/2026 En retard |
Proposé (simulation d'abord) : php artisan reprise:contrat-en-double 123 --garder=2026-116-C2 --retirer=2026-116-C1

php artisan reprise:contrat-en-double 123 --garder=2026-116-C2 --retirer=2026-116-C1
  • contrat 2026-116-C1 annulé, 1 échéance annulée (720,00 DH)
  • 3 relance(s) en attente écartée(s)          ← nombre de la copie ; la production peut différer
  • contrat 2026-116-C2 gardé (09/10/2026 → 08/04/2027) : ses échéances restent dues — rien n'a été encaissé sur aucun des deux
  • aucun mouvement d'argent
```

Idem 128 (`--garder=2026-121-C2 --retirer=2026-121-C1`, 1 échéance annulée,
720,00 DH, 2 relances sur la copie). **Après application** (base jetable) :
NOVERA et DEVANO doivent **720 DH** chacun — l'échéance n° 1 de leur C2 —, et
la commande rejouée répond « aucun contrat actif ne chevauche un autre ».

## 1.B — La cause et les process jumeaux

1. **Le formulaire « Modifier » du contrat** (onglet Contrats de la fiche,
   page d'édition du dossier) : quand le statut choisi est « Expiré »,
   « Annulé » ou « Résilié » et que des échéances resteraient dues, un choix
   **obligatoire** apparaît — « N échéance(s) non payée(s) resteront dues
   (… DH) : les annuler ? », la liste dessous, **« Annuler ces échéances » /
   « Les garder dues »**. Sans choix, l'enregistrement est refusé avec un
   message (jamais d'annulation muette, jamais de blocage muet). Nouveau
   service `App\Services\SortieDeContrat` :
   - échéances concernées : non payées, **sans paiement** (de l'argent derrière
     elle) et **sans facture** (elle se traite sur sa facture) ; « Annulé » →
     toutes ; « Expiré » posé avant la fin → toutes, après la fin → celles
     postérieures à la fin ; « Résilié » → celles postérieures à la date
     d'effet (ou à aujourd'hui) ;
   - annulation tracée (audit de chaque échéance), relances en attente
     écartées, caches du tableau de bord et du contrôle des factures oubliés.
   - **Les autres chemins** qui sortent un contrat traitaient déjà ses
     échéances, sans changement : la résiliation formelle
     (`PrononcerResiliation` → assainissement, avec les choix du gérant),
     « Classer sans suite… » (`ParcoursDemande`), `reprise:contrat-en-double`.
     Aucune commande ni aucune tâche planifiée ne pose « Expiré » : ce statut
     ne s'écrit qu'à la main, par ce formulaire.
2. **`mobdie:diagnostic`, famille 1** : nouvelle cause **« échéance due sur un
   contrat expiré/annulé »**, placée avant « jamais rien payé ». Geste : la
   commande du 1.A quand un contrat actif chevauche, sinon « fiche du client,
   onglet Contrats : « Modifier » le contrat … — l'aperçu propose d'annuler son
   échéance due ».

   **Sortie (base jetable, état du 16/10 reconstitué)** :
   avant v1.15.3 : « 63 client(s) « En retard » : 32 vraie dette, **19 jamais
   rien payé**, 7 résilié…, 3 paiement saisi hors échéancier, 2 contrat en
   double » ; avec v1.15.3 : « …, **17 jamais rien payé**, …, **2 échéance due
   sur un contrat expiré/annulé** », NOVERA et DEVANO à 1 440,00 DH avec la
   commande `reprise:contrat-en-double 123 --garder=2026-116-C2
   --retirer=2026-116-C1` (idem 128). **Attendu en production** : NOVERA et
   DEVANO dans cette cause, le compte des « jamais rien payé » baisse de 2.
   Après application des deux commandes, ils reviennent en « jamais rien payé »
   à 720 DH (leur C2 en retard, rien d'encaissé) : c'est leur vraie situation.
3. **Balayage sur la copie du 01/10** : **0 client** avec une échéance non
   payée sur un contrat `expire` ou `annule` — la copie n'a aucun contrat à ces
   statuts (96 actifs, 11 résiliés). Les deux cas connus sont nés après le
   01/10.

## 1.C — Recette

`tests/Feature/ContratExpireAvecDetteTest.php` (NOVERA reconstitué, 8 tests) :
liste « déjà marqué Expiré à la main » et commande proposée ; retrait propre
(contrat annulé, échéance annulée avec le motif, relance écartée, aucune
opération, audit, 720 DH dus, rejeu sans effet) ; résilié refusé avec le
message ; contrat retiré avec une échéance payée refusé ; passage à « Expiré » :
sans choix → erreur et rien d'écrit, « Les garder dues » → échéance due,
« Annuler » → échéance annulée ; contrat qui reste actif : aucune question ;
diagnostic : NOVERA dans la nouvelle cause avec la commande, et le geste de la
fiche quand aucun contrat actif ne chevauche.

## Fichiers

Modifiés : `app/Services/ContratEnDouble.php`,
`app/Console/Commands/Reprise/ContratEnDouble.php`,
`app/Services/DiagnosticDonnees.php`,
`app/Filament/Resources/DomiciliationClients/RelationManagers/ContractsRelationManager.php`.
Ajoutés : `app/Services/SortieDeContrat.php`,
`tests/Feature/ContratExpireAvecDetteTest.php`. Aucun déplacement, aucune
suppression.

Note : `./vendor/bin/pint` sans option reformate sept fichiers anciens hors du
lot (dont une migration et `bootstrap/app.php`) ; ils ont été laissés tels
quels pour ne pas gonfler le paquet — le lot est passé par `pint --dirty`.
