Bug Prêts différés

This commit is contained in:
ocroguennec committed 2026-09-26 16:09:02 +02:00
1 parent 26197fc411
commit e8e3ca34cc
1 file changed
+46 -26
+46 -26
View File
@@ -264,6 +264,10 @@ router.post('/fix-differe-dates', (req, res, next) => {
WHERE i.type_remb = 'differe' WHERE i.type_remb = 'differe'
AND i.date_souscription IS NOT NULL AND i.date_souscription IS NOT NULL
AND i.duree_mois 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); `).all(req.user.id);
// Approximation jours/mois (moyenne julienne) — cohérent avec l'ancien seuil fixe // 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`, `écart > ${seuilMois} mois avec date_souscription + duree_mois`,
}); });
// Régénère l'échéancier de projection avec la date corrigée — sans ça, // Régénère l'échéancier de projection avec la date corrigée, PUIS réconcilie contre
// simul_remboursements reste calé sur l'ancienne date aberrante. // les remboursements réels déjà enregistrés. Bug corrigé le 26/09/26 (dossier "Le
generateSimul(db, { // Mitra", statut déjà "Remboursé" depuis le 24/07/2025) : un generateSimul() direct
id: inv.id, // ignore tout remboursement déjà perçu et recrée toujours une échéance naïve à la
montant_investi: inv.montant_investi, // date corrigée — y compris pour un prêt intégralement soldé, dont la date corrigée
taux_interet: inv.taux_interet, // (date_souscription + duree_mois) tombait dans le futur. L'échéance orpheline
duree_mois: inv.duree_mois, // réapparaissait alors comme "à recevoir" dans le Dashboard et sur la fiche du prêt.
type_remb: inv.type_remb, // regenererEcheancier() applique le même correctif que POST/PUT /revisions (cf.
freq_interets: inv.freq_interets, // project_revision_correction) : pour un prêt différé (jamais de restructuration,
date_premiere_echeance: dateCalculee, // donc jamais de date_debut_simul), il rejoue l'historique des remboursements de
date_debut_simul: inv.date_debut_simul, // capital via replayAdjustSimulForActuals() après régénération, qui supprime toute
date_souscription: inv.date_souscription, // échéance future désormais caduque. Filet de sécurité en plus de l'exclusion des
echeance_fin_de_mois: inv.echeance_fin_de_mois ?? 0, // prêts clôturés/remboursés ajoutée ci-dessus (WHERE i.statut NOT IN (...)).
}); regenererEcheancier(inv.id);
corriges.push({ corriges.push({
id: inv.id, id: inv.id,
@@ -376,7 +380,7 @@ router.post('/fix-differe-dates', (req, res, next) => {
router.post('/check-echeancier-differe', (req, res, next) => { router.post('/check-echeancier-differe', (req, res, next) => {
try { try {
const rows = db.prepare(` 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.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 i.date_debut_simul, i.date_souscription, i.echeance_fin_de_mois
FROM investissements i FROM investissements i
@@ -393,15 +397,23 @@ router.post('/check-echeancier-differe', (req, res, next) => {
WHERE investissement_id = ? WHERE investissement_id = ?
ORDER BY numero_echeance ORDER BY numero_echeance
`); `);
const hasReinvest = db.prepare('SELECT 1 FROM reinvestissements WHERE investissement_id = ? LIMIT 1');
const corriges = []; const corriges = [];
for (const inv of rows) { for (const inv of rows) {
const simulRows = getSimul.all(inv.id); const simulRows = getSimul.all(inv.id);
const incoherent =
simulRows.length !== 1 || // Bug corrigé le 26/09/26 (dossier "Le Mitra") : un prêt déjà remboursé ou clôturé ne
simulRows[0].date_prevue !== inv.date_premiere_echeance; // 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; if (!incoherent) continue;
@@ -409,11 +421,19 @@ router.post('/check-echeancier-differe', (req, res, next) => {
? 'aucune échéance' ? 'aucune échéance'
: simulRows.map(s => `${s.date_prevue} (n°${s.numero_echeance})`).join(', '); : simulRows.map(s => `${s.date_prevue} (n°${s.numero_echeance})`).join(', ');
if (hasReinvest.get(inv.id)) { // regenererEcheancier() régénère PUIS réconcilie contre les remboursements réels déjà
generateSimulWithReinvestissements(db, inv.id); // enregistrés (replayAdjustSimulForActuals(), cf. project_revision_correction.md) — pour
} else { // un prêt soldé, cela supprime l'échéance orpheline ; pour un prêt actif dont
generateSimul(db, inv); // 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, { recordHistory(inv.id, {
type_evenement: 'correction_auto_echeancier', type_evenement: 'correction_auto_echeancier',
@@ -421,7 +441,7 @@ router.post('/check-echeancier-differe', (req, res, next) => {
champ: 'echeancier', champ: 'echeancier',
label: 'Échéancier de projection', label: 'Échéancier de projection',
ancienne_valeur: ancienEcheancier, 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)", 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, id: inv.id,
nom_projet: inv.nom_projet, nom_projet: inv.nom_projet,
ancien_echeancier: ancienEcheancier, ancien_echeancier: ancienEcheancier,
nouvelle_date: inv.date_premiere_echeance, nouvelle_date: nouvelleValeur,
}); });
} }