# RAPPORT v1.9.0 — L'argent se relie toujours, la pièce se lit toujours, le contrat se signe et se lit partout

**952 tests au vert (4 824 assertions)** — 902 à la v1.8.2, soit **50 tests
ajoutés**. Quatre commits, un par partie. Base : `v1.8.2` (`4739c6f`).

**Aucune dépendance composer ajoutée.** Une seule dépendance npm (`heic-to`),
compilée dans le paquet par `npm run build` — le serveur reçoit les assets
prêts, comme toujours.

---

## A. La chaîne argent dit toujours la vérité

### Le trou, et sa cause

« J'ai généré un devis pour un nouveau client, il m'a payé, j'ai généré la
facture depuis ce devis. L'opération a bien été saisie **mais elle n'était
reliée à aucune échéance**, et **ça ne m'a pas proposé de saisir une prime**. »

Cause confirmée : toute la mécanique prime/échéance était accrochée à
`DomiciliationInstallment`. Sans échéance, `rappelerSiSuggeree()` sortait
aussitôt, `montantSuggere()` rendait `null`, et `ReglerFactureAction` appelait
`ChampsPrime::enregistrer($data, …->installment)` **sans passer le paiement** —
une prime saisie naissait donc sans `payment_id` ni `client_id`. Une facture
groupée souffrait du même défaut : son lien est porté par les échéances, pas
par `installment_id`, qui vaut toujours `null`.

### Ce qui change

- **Toute facture qui s'encaisse propose une prime** — née d'un devis, née
  d'une échéance, groupée, ou encaissée hors trésorerie. Le barème pré-remplit
  quand une échéance en porte un ; sinon le champ reste proposé, en saisie
  libre.
- **La prime se rattache toujours** : au paiement qui vient de naître et, par
  lui ou par la facture, au client. `enregistrerPrime()` accepte désormais un
  `clientId` de repli. Plus jamais de prime orpheline.
- **La main pour corriger, même facture envoyée ou payée**, sans jamais toucher
  numéro ni montant :
  - `AffecterEcheanceAction` — affecter, ré-affecter, détacher. Le lien
    réciproque est posé (`invoices.installment_id` **et**
    `installment.invoice_id`), et si la facture est déjà payée, son paiement
    est propagé sur l'échéance, qui passe payée. Le détachement défait
    exactement l'inverse. Tout passe par `InvoiceService` — le journal n'est
    jamais touché directement.
  - `AjouterPrimeAction` s'ouvre depuis **trois portes** : l'échéance (comme
    avant), la **facture** et l'**opération du journal**.
  - **Corriger une prime** déjà saisie (bénéficiaire, montant) depuis l'écran
    des commissions, tant qu'elle n'est pas versée.
- **Le filet de rattrapage voit tout** : `encaissementsSansPrime()` inclut
  désormais les encaissements sans échéance (factures-devis, factures libres),
  et le compteur du tableau de bord les compte.

Le principe tient : tout cela **propose**, rien ne décide. Les échéances
candidates sont listées nominativement, les bénéficiaires sont choisis.

---

## B. L'OCR lit tout, même mal scanné

- **Une divergence de nom n'efface plus rien d'autre.** La règle métier tient —
  le nom du contrat / du certificat négatif prévaut, et ni le nom complet ni
  ses deux moitiés ne se proposent — mais le numéro de pièce, les dates et
  l'adresse **s'appliquent quand même**, chaque ligne portant l'avertissement
  pour arbitrage. Auparavant, tout était jeté.
- **Une réponse illisible devient un échec explicite.** `LectureIllisibleException`
  porte un message actionnable (« Reprenez le scan… »), l'analyse se marque
  **en échec** au lieu de « terminé, 0 champ ». Et `parserJson()` est devenu
  tolérant : bloc Markdown, phrase d'introduction, virgule traînante,
  guillemets typographiques — quatre façons d'écrire un JSON juste que
  `json_decode()` refusait.
- **Escalade au modèle le plus puissant** (décision de l'exploitant) : sur
  échec **ou lecture vide seulement**, jamais en routine, et **bloquée si le
  plafond mensuel ne le permet pas**. Les jetons des deux essais sont comptés.
  Pas d'escalade en mode direct — elle doublerait le gel de l'écran.
- **Les images sont normalisées avant l'envoi** (`App\Services\AI\ImagePourIa`,
  traversé par `PieceTextExtractor`, donc **des deux côtés** — assistant comme
  fiche client, ce qui était précisément le défaut) : HEIC converti
  (Imagick / `heif-convert` / `sips`, détectés à l'exécution), redressement
  EXIF, réduction sous 1 600 px, contraste relevé et netteté légère. Repli
  propre partout : ce qui ne peut pas être traité part tel quel.
- **Côté navigateur**, `resources/js/pieces-avant-envoi.js` convertit les HEIC
  et allège les photos avant l'upload (`heic-to` en import dynamique : le
  chunk de 3 Mo n'est chargé que si un HEIC se présente).
- **Recto et verso partent en un seul appel** : `analyserFichier()` accepte
  plusieurs chemins, `construireContenu()` plusieurs blocs source, et les jobs
  regroupent les faces d'une même pièce d'identité.
- **Quatre nouveaux types de pièce** : carte de résident / d'immatriculation
  étrangère, récépissé de titre de séjour, carte consulaire, permis de
  conduire. Reconnus par l'OCR, offerts dans les trois `Select`, nommés dans
  l'archive, et gravité d'expiration adaptée.
- **L'OCR s'étend à tout document** : statuts, modèle J, pièce « autre »
  proposent raison sociale, ICE, RC, ville du RC, tribunal, activité,
  représentant et son titre — toujours en proposition validée.

---

## C. Le contrat : sa signature, son signataire, son devis, son renouvellement

- **Le nom et le titre du signataire figurent DANS la case de signature**, des
  deux côtés. La fonction du gérant se lit dans `setting('gerant.fonction')` —
  « gérant » n'est plus écrit en dur. Le cachet passe de 2,1 à 1,5 cm pour
  faire place sans allonger le cadre : **le contrat tient toujours sur deux
  pages** (un test le vérifie sur le PDF réel).
- **Forme juridique du client** (migration additive) : SARL, SARL AU, SNC, SCS,
  société civile, SAS, SASU, SA, association, coopérative, GIE, succursale,
  autre. Elle propose le titre par défaut selon la forme **et la civilité** —
  Gérant/Gérante, Président/Présidente. **Le titre saisi prime toujours**, et
  une forme non renseignée laisse le libellé historique : les dossiers repris
  ne bougent pas.
- **Le devis en parallèle est actif par défaut** dans les **trois** flux :
  assistant, action « Ajouter un contrat », relation-manager des contrats.
  Créé en brouillon, à relire avant envoi ; décochable d'un clic.
- **Contrat non renouvelable** : troisième valeur de `mode_reconduction`,
  offerte dans les trois flux. La clause du gabarit devient
  `{{clause_renouvellement}}`, et `RenouvellementService::preparerEcheant()`
  ignore les contrats non renouvelables.

> Au passage : `ContratClientExistantAction` mélangeait `Carbon\Carbon` et
> `Illuminate\Support\Carbon`. Le `TypeError` partait droit dans un `rescue()`
> silencieux — le devis n'aurait jamais été créé. Corrigé, et l'échec se
> journalise désormais.

---

## D. Le mobile débloqué, et l'app qui ne se fige plus

- **Les trois entrées PDF passent par le lecteur** : relevé de compte courant
  (action **et** bouton de la vue), aperçu d'une pièce classée, aperçu avant
  enregistrement. L'aperçu d'une pièce gagne **« Télécharger »** et
  **« Ouvrir en grand »**.
- **L'envoi par e-mail s'étend au relevé de compte courant** (le contrat et
  l'attestation l'avaient déjà, par `EnvoyerPieceAction`) : nouveau type de
  pièce `releve_compte_courant`, PDF **produit à l'envoi** comme celui d'une
  facture, inscrit au registre du courrier.
- **L'app ne gèle plus** :
  - l'assistant **n'exécute plus d'analyse synchrone** quand la file est
    arrêtée — c'était jusqu'à 45 secondes d'écran mort au milieu d'une saisie,
    la cause la plus probable des blocages signalés. L'analyse attend son tour,
    l'écran reste utilisable, la saisie manuelle fonctionne tout de suite ;
  - le mode direct **explicite** (case « Analyser maintenant ») passe de 45 à
    **25 secondes** ;
  - une requête de plus de **12 secondes** laisse une ligne nommée au journal
    (chemin, composant Livewire, durée, utilisateur) — de quoi pincer un futur
    cas au lieu de le chercher.

---

## Ce qu'il faut jouer au déploiement

1. **Sauvegarde phpMyAdmin** de `mobdiema_mobdie_app` (deux migrations).
2. Téléverser le tarball, **Extract**. **Aucun fichier supprimé ni renommé**
   entre v1.8.2 et v1.9.0 : rien à effacer sur le serveur.
3. `$PHP artisan migrate --force` — deux migrations, **additives et
   idempotentes** :
   - `2026_09_08_100000_la_forme_juridique_du_domicilie` : ajoute une colonne
     nullable ; ne touche aucune donnée.
   - `2026_09_08_110000_la_clause_de_renouvellement_devient_variable` :
     remplacement **ciblé** de la phrase de tacite reconduction par
     `{{clause_renouvellement}}` dans les gabarits stockés, **seulement si la
     phrase exacte y figure**. Une retouche manuelle n'est jamais écrasée, et
     deux passages ne font qu'un seul remplacement.
4. `optimize:clear`, `config:cache`, `route:cache`, `view:cache`.
5. **Aucune commande de reprise** dans ce lot.

## Quoi vérifier à l'écran

- **Chaîne argent** : encaisser une facture née d'un devis → le champ « Prime
  pour » est proposé ; la prime saisie apparaît dans l'écran des commissions
  avec son client. Sur une facture payée, le menu offre « Affecter à une
  échéance » et « Ajouter une prime ».
- **OCR** : déposer une CIN en **deux images** (recto + verso) → un seul
  résultat, champs fusionnés. Déposer une photo iPhone (HEIC) → elle est
  acceptée et lue. Déposer des statuts → des champs de société sont proposés.
- **Contrat** : générer un contrat → le nom du gérant et sa fonction sont
  **dans** le cadre de signature, celui du représentant aussi ; le document
  tient sur deux pages. Créer un contrat avec « Non renouvelable » → l'article 4
  porte la clause correspondante.
- **Réglages → Gérant** : vérifier que « Fonction du gérant » porte bien le
  titre voulu — c'est lui qui s'imprime maintenant.
- **Mobile (iPhone)** : ouvrir un relevé de compte courant, une pièce classée,
  un aperçu de document → chacun s'ouvre dans le lecteur, avec « ← Retour » et
  « Télécharger » qui fonctionne. Envoyer un relevé par e-mail depuis les
  comptes courants.
