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'
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,
});
}