diff --git a/backend/src/routes/investissements.js b/backend/src/routes/investissements.js index 91288cd..40ee6ef 100644 --- a/backend/src/routes/investissements.js +++ b/backend/src/routes/investissements.js @@ -264,6 +264,10 @@ router.post('/fix-differe-dates', (req, res, next) => { WHERE i.type_remb = 'differe' AND i.date_souscription IS NOT NULL AND i.duree_mois IS NOT NULL + -- Bug corrigé le 26/09/26 (dossier "Le Mitra") : un prêt déjà remboursé/clôturé n'a + -- plus lieu d'avoir sa date d'échéance "corrigée" — cf. commentaire plus bas sur + -- regenererEcheancier() pour le détail du bug que cette exclusion referme. + AND i.statut NOT IN ('rembourse', 'cloture') `).all(req.user.id); // Approximation jours/mois (moyenne julienne) — cohérent avec l'ancien seuil fixe @@ -327,20 +331,20 @@ router.post('/fix-differe-dates', (req, res, next) => { `écart > ${seuilMois} mois avec date_souscription + duree_mois`, }); - // Régénère l'échéancier de projection avec la date corrigée — sans ça, - // simul_remboursements reste calé sur l'ancienne date aberrante. - generateSimul(db, { - id: inv.id, - montant_investi: inv.montant_investi, - taux_interet: inv.taux_interet, - duree_mois: inv.duree_mois, - type_remb: inv.type_remb, - freq_interets: inv.freq_interets, - date_premiere_echeance: dateCalculee, - date_debut_simul: inv.date_debut_simul, - date_souscription: inv.date_souscription, - echeance_fin_de_mois: inv.echeance_fin_de_mois ?? 0, - }); + // Régénère l'échéancier de projection avec la date corrigée, PUIS réconcilie contre + // les remboursements réels déjà enregistrés. Bug corrigé le 26/09/26 (dossier "Le + // Mitra", statut déjà "Remboursé" depuis le 24/07/2025) : un generateSimul() direct + // ignore tout remboursement déjà perçu et recrée toujours une échéance naïve à la + // date corrigée — y compris pour un prêt intégralement soldé, dont la date corrigée + // (date_souscription + duree_mois) tombait dans le futur. L'échéance orpheline + // réapparaissait alors comme "à recevoir" dans le Dashboard et sur la fiche du prêt. + // regenererEcheancier() applique le même correctif que POST/PUT /revisions (cf. + // project_revision_correction) : pour un prêt différé (jamais de restructuration, + // donc jamais de date_debut_simul), il rejoue l'historique des remboursements de + // capital via replayAdjustSimulForActuals() après régénération, qui supprime toute + // échéance future désormais caduque. Filet de sécurité en plus de l'exclusion des + // prêts clôturés/remboursés ajoutée ci-dessus (WHERE i.statut NOT IN (...)). + regenererEcheancier(inv.id); corriges.push({ id: inv.id, @@ -376,7 +380,7 @@ router.post('/fix-differe-dates', (req, res, next) => { router.post('/check-echeancier-differe', (req, res, next) => { try { const rows = db.prepare(` - SELECT i.id, i.nom_projet, i.date_premiere_echeance, i.date_cible, + SELECT i.id, i.nom_projet, i.statut, i.date_premiere_echeance, i.date_cible, i.montant_investi, i.taux_interet, i.duree_mois, i.type_remb, i.freq_interets, i.date_debut_simul, i.date_souscription, i.echeance_fin_de_mois FROM investissements i @@ -393,15 +397,23 @@ router.post('/check-echeancier-differe', (req, res, next) => { WHERE investissement_id = ? ORDER BY numero_echeance `); - const hasReinvest = db.prepare('SELECT 1 FROM reinvestissements WHERE investissement_id = ? LIMIT 1'); const corriges = []; for (const inv of rows) { const simulRows = getSimul.all(inv.id); - const incoherent = - simulRows.length !== 1 || - simulRows[0].date_prevue !== inv.date_premiere_echeance; + + // Bug corrigé le 26/09/26 (dossier "Le Mitra") : un prêt déjà remboursé ou clôturé ne + // doit conserver AUCUNE échéance de projection — même une échéance unique dont la date + // correspond bien à date_premiere_echeance est une échéance ORPHELINE dans ce cas (le + // prêt est soldé), pas un état cohérent. Sans ce garde-fou, cette route n'aurait jamais + // détecté l'échéance laissée par le bug de "Corriger les dates des prêts différés" + // (ci-dessus) sur un prêt déjà soldé, puisque cette échéance orpheline correspondait + // justement (par construction du bug) à date_premiere_echeance. + const estClos = inv.statut === 'rembourse' || inv.statut === 'cloture'; + const incoherent = estClos + ? simulRows.length > 0 + : (simulRows.length !== 1 || simulRows[0].date_prevue !== inv.date_premiere_echeance); if (!incoherent) continue; @@ -409,11 +421,19 @@ router.post('/check-echeancier-differe', (req, res, next) => { ? 'aucune échéance' : simulRows.map(s => `${s.date_prevue} (n°${s.numero_echeance})`).join(', '); - if (hasReinvest.get(inv.id)) { - generateSimulWithReinvestissements(db, inv.id); - } else { - generateSimul(db, inv); - } + // regenererEcheancier() régénère PUIS réconcilie contre les remboursements réels déjà + // enregistrés (replayAdjustSimulForActuals(), cf. project_revision_correction.md) — pour + // un prêt soldé, cela supprime l'échéance orpheline ; pour un prêt actif dont + // l'échéancier était simplement désynchronisé, cela le reconstruit SANS écraser un + // éventuel remboursement anticipé partiel déjà pris en compte, contrairement à l'ancien + // appel direct à generateSimul()/generateSimulWithReinvestissements() ci-dessous, qui ne + // réconciliait jamais rien. + regenererEcheancier(inv.id); + + const nouvelEcheancier = getSimul.all(inv.id); + const nouvelleValeur = nouvelEcheancier.length === 0 + ? 'aucune échéance (prêt soldé)' + : nouvelEcheancier.map(s => `${s.date_prevue} (n°${s.numero_echeance})`).join(', '); recordHistory(inv.id, { type_evenement: 'correction_auto_echeancier', @@ -421,7 +441,7 @@ router.post('/check-echeancier-differe', (req, res, next) => { champ: 'echeancier', label: 'Échéancier de projection', ancienne_valeur: ancienEcheancier, - nouvelle_valeur: inv.date_premiere_echeance, + nouvelle_valeur: nouvelleValeur, }], notes: "Correction automatique (Nettoyage > Vérifier la cohérence de l'échéancier des prêts différés)", }); @@ -430,7 +450,7 @@ router.post('/check-echeancier-differe', (req, res, next) => { id: inv.id, nom_projet: inv.nom_projet, ancien_echeancier: ancienEcheancier, - nouvelle_date: inv.date_premiere_echeance, + nouvelle_date: nouvelleValeur, }); }