Sauvegarde de la mémoire

This commit is contained in:
2026-07-14 15:40:57 +02:00
parent 3a9de15526
commit f9c5316303
+48 -1
View File
@@ -1,5 +1,5 @@
# MEMORY.md — Crowdlending Tracker
*Dernière mise à jour: 2026-07-13 (session 13)*
*Dernière mise à jour: 2026-07-14 (session 14)*
---
@@ -633,3 +633,50 @@ Le champ `type` est prévu pour réutiliser la table plus tard (ex. `'rendement_
### Piège outillage — vérification syntaxique de fichiers JSX volumineux
Le mount bash reste périmé (cf. sessions précédentes). Pour vérifier un **nouveau** composant JSX isolé (pas une édition dans un fichier existant de 1000+ lignes), la méthode fiable trouvée cette session : `npm init -y && npm install esbuild` dans `/tmp` (indépendant du node_modules Windows du projet, incompatible avec le sandbox Linux), copier le contenu exact du fichier via `cat > fichier << 'EOF'` puis `esbuild fichier.jsx --bundle --format=esm --jsx=automatic --external:react --outfile=...`. Pour une **édition ponctuelle** dans un gros fichier existant, la relecture `Read` ciblée des zones modifiées (comptage d'accolades/balises JSX) reste suffisante et plus rapide.
---
## Session 14 — Import IA (prompt + bugs), modale doublons, purge plateforme, job données incomplètes (2026-07-14)
### Prompt IA import Remboursements — itérations successives (`ImportsSection.jsx`, `buildReferenceSection`/`FIELD_HINTS_OVERRIDE`)
1. **Plateforme unique** : si une seule plateforme existe, le prompt l'assigne directement à toutes les lignes sans poser de question (branche `plats.length === 1` dans `buildReferenceSection`).
2. **Règle CAPITAL vs INTÉRÊTS** : distingue une ligne "remboursement mensualité" unique (montant mêlant capital+intérêts) via le ratio (prélèvements sociaux + IR) ÷ montant total : proche de 30 % → intérêts purs ; nettement inférieur → reconstitue `interets_bruts ≈ moyenne(prelev_sociaux/0.172, prelev_forfaitaire/0.128)` puis `capital = total interets_bruts` ; aucun prélèvement adjacent → capital pur (échéance finale in fine/différé).
3. **Identification cashback/bonus** (ajoutée suite à un cas réel : "Rémunération code cadeau") : reconnue par le **libellé** (mots-clés "cashback", "bonus", "prime", "code cadeau", "parrainage"...), jamais par l'absence de prélèvement seule (un remboursement de capital pur n'a lui non plus aucun prélèvement adjacent — ne pas confondre). Si le libellé ne référence aucun projet suivi (ex. parrainage global) → ligne exclue du JSON (le module d'import exige un `investissement_id`).
4. **Règle critique présence systématique des champs** : imposer que les 5 champs (`capital`, `cashback`, `interets_bruts`, `prelev_sociaux`, `prelev_forfaitaire`) soient toujours explicitement présents sur CHAQUE ligne (valeur 0 si non applicable), jamais omis — corrige un vrai bug de détection (voir ci-dessous).
5. Suppression de la consigne "génère aussi `net_recu`" (champ jamais lu par le backend, recalculé côté serveur depuis capital/cashback/intérêts/prélèvements) — source de confusion sans utilité.
### 🔴 Bug trouvé et corrigé — détection des colonnes basée uniquement sur la 1ère ligne du JSON
- **Symptôme réel** : un remboursement de capital final (250 €) importé à 0,00 € partout dans l'app, alors que le JSON source contenait bien `"capital": 250`.
- **Cause** : `POST /imports/preview` calcule `headers = Object.keys(rows[0])` — seule la première ligne du fichier sert à détecter les colonnes disponibles pour l'auto-mapping. Si un champ (ex. `capital`) est absent de la première ligne (fréquent avec les JSON générés par IA où seules certaines lignes ont telle ou telle info) mais présent plus loin, il n'est **jamais mappé** pour tout le fichier — donc toujours lu comme `0`, silencieusement, même sur les lignes où il est renseigné.
- **Fix** : uniquement via le prompt (règle 4 ci-dessus, pas de changement de code) — imposer à l'IA génératrice de toujours inclure tous les champs optionnels sur chaque ligne, à 0 par défaut.
### 🔴 Bug trouvé et corrigé — champ `cashback` absent du schéma d'import Remboursements
- **Symptôme réel** : une ligne de cashback (`"cashback": 2.5`) importée avec `cashback: 0,00 €` alors que tous les autres champs de mapping fonctionnaient.
- **Cause, différente du bug précédent** : `MODULES.remboursements.optional` (`ImportsSection.jsx`) ne listait jamais `cashback` parmi les champs du module — ni pour l'auto-mapping (`runPreview`), ni pour le mapping manuel (`<select>`), ni pour la liste de champs générée dans le prompt IA (`buildFieldsList`). Le champ existe pourtant en base et dans le formulaire manuel de saisie. Bug préexistant, pas introduit cette session.
- **Fix** : ajout de `'cashback'` à `MODULES.remboursements.optional`.
- **Point d'attention** : les lignes déjà importées avec cashback perdu (0 au lieu du vrai montant) ne sont pas corrigées rétroactivement — la détection de doublon remboursements ne compare pas le cashback, donc un ré-import est soit ignoré comme doublon, soit crée une ligne en double si on force l'acceptation. Correction manuelle ligne par ligne recommandée dans ce cas.
### 🔴 Bug trouvé et corrigé — l'import de remboursements ne reproduit pas la logique de la saisie manuelle
- **Symptôme réel** (2 captures d'écran comparées, dev vs prod) : un investissement soldé (capital intégralement remboursé) via **import** restait affiché `en_cours` avec un échéancier complet non réajusté (22 échéances futures inchangées), alors que le même remboursement saisi **manuellement** passait bien l'investissement à `rembourse` et tronquait l'échéancier aux échéances réellement dues (5 au lieu de 22).
- **Cause** : `POST /remboursements` (saisie manuelle, `routes/remboursements.js`) appelle après chaque insertion `syncInvestissementStatut()` (passe `rembourse` si capital total remboursé ≥ montant investi + réinvestissements) et `adjustSimulForActuals()` (tronque/recalcule les échéances futures devenues caduques en cas de remboursement anticipé). `POST /imports/apply` (module `remboursements`) ne faisait qu'un `INSERT` brut en boucle, sans jamais appeler ces deux fonctions.
- **Fix** : `syncInvestissementStatut` exportée depuis `remboursements.js` ; `imports.js` importe cette fonction + `adjustSimulForActuals` (déjà exportée de `schedule.js`) ; après le commit de la transaction d'import (module `remboursements`), boucle best-effort sur tous les `investissement_id` touchés par l'import pour appeler les deux fonctions — reproduit exactement le comportement de la saisie manuelle.
- **Règle à retenir** : toute route d'import en masse qui insère directement dans une table déjà pourvue d'effets de bord post-insertion (recalcul de statut, régénération d'échéancier...) côté route manuelle doit rejouer ces mêmes effets de bord après le commit — ne pas se contenter du simple `INSERT`.
### Modale de revue des doublons à l'import (tous modules)
- Nouvelle route `POST /imports/check-duplicates` (dry-run, même détection que `/apply` mais sans écriture) + `duplicateDecisions` accepté par `/apply` (`{ [rowNum]: 'accept'|'skip' }`).
- Détection des doublons **internes au fichier** (pas seulement contre la base) via des `Map` en mémoire (`seenDepotsRetraits`, `seenInvestissements`, etc.) qui simulent l'effet séquentiel d'une transaction SQLite réelle (une ligne répétée plus loin dans le même fichier est comparée aux lignes déjà "vues", pas seulement à la base).
- UI (`ImportsSection.jsx`) : ligne = case à cocher + titre "Doublon de données repéré en ligne X avec celles de la ligne Y (ou un enregistrement déjà en base)", détail replié par défaut (chevron), boutons "Cocher/Décocher tous les doublons".
- **Pièges CSS rencontrés** (`styles.css` a des sélecteurs globaux `label`/`input` qui fuient sur du HTML brut) : `label { text-transform: uppercase; ...}` et `input,select,textarea { width:100%; padding:7px 10px; ...}` s'appliquent même à une checkbox de ligne — toujours réinitialiser explicitement en inline style (`textTransform:'none'`, `width:14, height:14, padding:0, flexShrink:0`), pattern déjà présent ailleurs (`.cat-select-item input[type="checkbox"]`). `.modal-overlay`/`.modal`/`.modal-header` étaient absentes de `styles.css` (seule `.modal-backdrop` existait, pour le composant `Modal.jsx` partagé) — ajoutées, corrige aussi rétroactivement les modales ad-hoc de `DataCleanupSection.jsx`.
- Après import réussi : le textarea "Analyser les données" (IA) se vide et la page recharge automatiquement (message de résultat conservé via `sessionStorage` le temps du reload, réaffiché au montage).
### Job horaire — données essentielles manquantes sur les investissements
- Nouveau `backend/src/jobs/checkDonneesIncompletes.js`, démarré dans `server.js` (`startCheckDonneesIncompletesJob`, pattern horaire identique à `autoTicketStatus.js`).
- Vérifie sur chaque investissement `statut != 'cloture'` : `taux_interet`, `duree_mois`, `type_remb`, `date_premiere_echeance`.
- **Anti-doublon de notification** : nouvelle colonne `investissements.donnees_incompletes_signature` (migration) stockant la liste triée des champs actuellement manquants. Notification renvoyée seulement si cette signature change (nouveau manque, ou toujours incomplet mais différemment) ; réinitialisée silencieusement (sans notif) quand tout redevient complet.
- Même fonction rejouée en best-effort à la fin d'un import `investissements` réussi (`imports.js`, après le commit de la transaction).
- Scope volontairement limité à ces 4 champs (portée assumée sans élargir sur le "etc." de la demande initiale).
### Suppression de données d'une plateforme (`DataCleanupSection.jsx` + `POST /plateformes/:id/purge-donnees`)
- Scopes : toutes les données / dépôts-retraits / investissements (+ remboursements liés en cascade) / remboursements uniquement. La fiche plateforme n'est jamais supprimée.
- **Confirmation par PIN à 6 chiffres** (remplace le "retapez le nom de la plateforme") : PIN aléatoire généré à l'ouverture de la modale (`Math.floor(100000 + Math.random()*900000)`), affiché en gros (monospace 30px, espacé, rouge) ; le payload envoyé à l'API contient toujours `confirmNom: purgePlat.nom` en interne (le PIN est une couche de confirmation UI uniquement, la vérification serveur par nom exact est inchangée).
- **Sélecteur de plateforme corrigé pour le multi-détenteur** : deux plateformes de familles différentes peuvent porter le même nom (ex. deux comptes "Enky"). Fix en reprenant le pattern déjà établi ailleurs (`const multiDetenteurPlats = new Set(plats.map(p => p.investisseur_id)).size > 1`, suffixe `— {investisseur_nom}` dans les `<option>` et rappel dans le texte de la modale, uniquement si multi-détenteur) — cf. session 9 "Colonne Détenteur — masquée si mono-détenteur", même pattern à répliquer sur tout futur select de plateformes.