diff --git a/backend/src/routes/investissements.js b/backend/src/routes/investissements.js index f9e54a5..8a7825a 100644 --- a/backend/src/routes/investissements.js +++ b/backend/src/routes/investissements.js @@ -73,7 +73,8 @@ const PerteSchema = z.object({ function regenererEcheancier(investissementId) { const inv = db.prepare(` SELECT id, montant_investi, taux_interet, duree_mois, type_remb, freq_interets, - date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois + date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois, + date_cible FROM investissements WHERE id = ? `).get(investissementId); if (!inv) return; diff --git a/backend/src/utils/schedule.js b/backend/src/utils/schedule.js index 8bd9aca..a8d447a 100644 --- a/backend/src/utils/schedule.js +++ b/backend/src/utils/schedule.js @@ -125,7 +125,8 @@ export function monthsDiff(isoA, isoB) { export function adjustSimulForActuals(db, investissementId) { const inv = db.prepare(` SELECT id, montant_investi, taux_interet, duree_mois, type_remb, freq_interets, - date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois + date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois, + date_cible FROM investissements WHERE id = ? `).get(investissementId); @@ -433,7 +434,8 @@ function adjustFirstPartialPeriod(db, investissementId) { export function generateSimulWithReinvestissements(db, investissementId) { const inv = db.prepare(` SELECT id, montant_investi, taux_interet, duree_mois, type_remb, freq_interets, - date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois + date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois, + date_cible FROM investissements WHERE id = ? `).get(investissementId); @@ -452,6 +454,31 @@ export function generateSimulWithReinvestissements(db, investissementId) { // taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals) if (inv.taux_interet == null || !inv.duree_mois) return; + // Cas particulier "differe" (versement unique), avec réinvestissements : une seule échéance + // au total, ancrée sur date_cible (la date d'effet de la révision la plus récente, jamais + // recalculée à partir d'un décompte de mois écoulés — cf. commentaire équivalent, et le + // bug qu'il corrige, dans generateSimul() plus bas). + if ((inv.type_remb || 'in_fine') === 'differe') { + const finDeMoisDiffere = !!inv.echeance_fin_de_mois; + const startDate = inv.date_cible || inv.date_premiere_echeance || inv.date_souscription; + if (!startDate) return; + const date = finDeMoisDiffere ? lastDayOfMonth(startDate) : startDate; + const capital = round2( + inv.montant_investi + + reinvests.filter(r => r.date_reinvestissement <= date).reduce((s, r) => s + r.montant, 0) + ); + const interets = round2(capital * (inv.taux_interet / 100 / 12) * inv.duree_mois); + db.transaction(() => { + db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(investissementId); + db.prepare(` + INSERT INTO simul_remboursements + (investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu) + VALUES (?,?,?,?,?,?) + `).run(investissementId, 1, date, capital, interets, round2(capital + interets)); + })(); + return; + } + const finDeMois = !!inv.echeance_fin_de_mois; const type = inv.type_remb || 'in_fine'; const freq = inv.freq_interets || 'mensuel'; @@ -609,12 +636,44 @@ export function generateSimulWithReinvestissements(db, investissementId) { */ export function generateSimul(db, inv) { const { id, montant_investi, taux_interet, duree_mois, type_remb, freq_interets, - date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois } = inv; + date_premiere_echeance, date_debut_simul, date_souscription, echeance_fin_de_mois, + date_cible } = inv; // taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals) if (taux_interet == null || !duree_mois) return; const finDeMois = !!echeance_fin_de_mois; + + // Cas particulier "differe" (versement unique) : il n'existe jamais qu'une seule échéance + // au total, donc le mécanisme de restructuration ci-dessous (numérotation par mois écoulés + // depuis date_premiere_echeance, conservation d'échéances déjà honorées) n'a pas de sens — + // il a été conçu pour les prêts à échéances multiples (in_fine/amortissable) où l'on + // reprend le calendrier en cours de route. Appliqué tel quel à un prêt différé après une + // révision (ex. prolongation), il produisait un numero_echeance et une date aberrants : + // numero_echeance = mois écoulés + 1 (ex. n°10) au lieu de 1, et une date recalculée à + // partir de l'ancienne cadence (date_premiere_echeance + mois écoulés) au lieu de la + // nouvelle date cible — bug signalé par Olivier le 22/08/26 sur le dossier "Le Dolce + // Notte". On régénère donc toujours une unique échéance, ancrée sur date_cible (la date + // d'effet de la révision la plus récente, ou la date de première échéance d'origine si + // aucune révision n'a encore eu lieu). + if ((type_remb || 'in_fine') === 'differe') { + const startDate = date_cible || date_premiere_echeance || date_souscription; + if (!startDate) return; + const [echeance] = buildSchedule({ + montant: montant_investi, taux: taux_interet, duree: duree_mois, + type: 'differe', freq: freq_interets || 'in_fine', startDate, finDeMois, + }); + db.transaction(() => { + db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(id); + db.prepare(` + INSERT INTO simul_remboursements + (investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu) + VALUES (?,?,?,?,?,?) + `).run(id, 1, echeance.date, echeance.capital, echeance.interets, echeance.total); + })(); + return; + } + const isRestructuration = !!date_debut_simul; // En mode restructuration, on a besoin de connaître les échéances déjà honorées