Initial commit
This commit is contained in:
@@ -0,0 +1,572 @@
|
||||
/**
|
||||
* Shared schedule builder — used by both simul.js and investissements.js
|
||||
*/
|
||||
|
||||
function addMonths(isoDate, months) {
|
||||
const [y, m, d] = isoDate.split('-').map(Number);
|
||||
const dt = new Date(Date.UTC(y, m - 1 + months, d));
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le dernier jour du mois de isoDate.
|
||||
* Ex : '2024-01-15' → '2024-01-31', '2024-02-01' → '2024-02-29'
|
||||
*/
|
||||
function lastDayOfMonth(isoDate) {
|
||||
const [y, m] = isoDate.split('-').map(Number);
|
||||
const dt = new Date(Date.UTC(y, m, 0)); // jour 0 du mois suivant = dernier jour du mois courant
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute n mois à isoDate et positionne le résultat sur le dernier jour du mois cible.
|
||||
* Évite les débordements de JavaScript (ex : 31 jan + 1 mois = 29 fév, pas 3 mar).
|
||||
* Ex : addMonthsEOM('2024-01-31', 1) → '2024-02-29'
|
||||
*/
|
||||
function addMonthsEOM(isoDate, months) {
|
||||
const [y, m] = isoDate.split('-').map(Number);
|
||||
const dt = new Date(Date.UTC(y, m - 1 + months + 1, 0)); // jour 0 = dernier du mois cible
|
||||
return dt.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const round2 = n => Math.round(n * 100) / 100;
|
||||
|
||||
/**
|
||||
* Builds a payment schedule.
|
||||
*
|
||||
* type:
|
||||
* - in_fine : intérêts périodiques selon freq, capital à l'échéance
|
||||
* - amortissable: mensualité/trimestrialité constante (modèle bancaire)
|
||||
* - differe : aucun versement intermédiaire, tout en une fois à l'échéance
|
||||
*
|
||||
* freq:
|
||||
* - mensuel : une échéance par mois (offset i-1)
|
||||
* - trimestriel : une échéance tous les 3 mois
|
||||
* - in_fine : forcé pour differe
|
||||
*
|
||||
* startDate = date_premiere_echeance (1ère échéance tombe sur cette date, offset 0)
|
||||
*/
|
||||
export function buildSchedule({ montant, taux, duree, type, freq, startDate, finDeMois = false }) {
|
||||
const step = freq === 'trimestriel' ? 3 : 1;
|
||||
const rPer = (taux / 100 / 12) * step;
|
||||
const nPer = freq === 'in_fine' ? 1 : Math.round(duree / step);
|
||||
const out = [];
|
||||
|
||||
// Calcule la date d'une échéance selon le mode fin-de-mois ou non.
|
||||
const dateAt = (base, offsetMonths) =>
|
||||
finDeMois ? addMonthsEOM(base, offsetMonths) : addMonths(base, offsetMonths);
|
||||
|
||||
if (type === 'differe') {
|
||||
const interets = round2(montant * (taux / 100 / 12) * duree);
|
||||
// Pour un prêt différé, date_premiere_echeance == date_cible (versement unique).
|
||||
// startDate est déjà la bonne date ; en mode fin-de-mois on s'assure qu'elle
|
||||
// correspond bien au dernier jour (idempotent si déjà positionné par le frontend).
|
||||
out.push({
|
||||
n: 1,
|
||||
date: finDeMois ? lastDayOfMonth(startDate) : startDate,
|
||||
capital: round2(montant),
|
||||
interets,
|
||||
total: round2(montant + interets),
|
||||
});
|
||||
|
||||
} else if (type === 'in_fine') {
|
||||
const interetsPer = round2(montant * rPer);
|
||||
for (let i = 1; i <= nPer; i++) {
|
||||
const isLast = i === nPer;
|
||||
out.push({
|
||||
n: i,
|
||||
date: dateAt(startDate, (i - 1) * step),
|
||||
capital: isLast ? round2(montant) : 0,
|
||||
interets: interetsPer,
|
||||
total: round2(interetsPer + (isLast ? montant : 0)),
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
// amortissable
|
||||
const a = rPer === 0
|
||||
? montant / nPer
|
||||
: montant * rPer / (1 - Math.pow(1 + rPer, -nPer));
|
||||
let restant = montant;
|
||||
for (let i = 1; i <= nPer; i++) {
|
||||
const interets = round2(restant * rPer);
|
||||
const capital = round2(a - interets);
|
||||
restant = round2(restant - capital);
|
||||
out.push({
|
||||
n: i,
|
||||
date: dateAt(startDate, (i - 1) * step),
|
||||
capital,
|
||||
interets,
|
||||
total: round2(a),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Nombre de mois entiers entre deux dates ISO */
|
||||
function monthsDiff(isoA, isoB) {
|
||||
const a = new Date(isoA);
|
||||
const b = new Date(isoB);
|
||||
return (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalcule les simul_remboursements FUTURS en tenant compte des remboursements
|
||||
* de capital déjà enregistrés (remboursement anticipé partiel) ET des réinvestissements.
|
||||
*
|
||||
* - Les entrées dont date_prevue <= dernière date de remboursement capital sont laissées intactes.
|
||||
* - Les entrées suivantes sont recalculées sur la base du capital restant dû.
|
||||
* - Si aucun capital n'a encore été remboursé → régénération complète.
|
||||
* - Les réinvestissements planifiés après la dernière date de remboursement sont pris en compte
|
||||
* pour gonfler progressivement le capital des périodes futures.
|
||||
*/
|
||||
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
|
||||
FROM investissements WHERE id = ?
|
||||
`).get(investissementId);
|
||||
|
||||
if (!inv || !inv.taux_interet || !inv.duree_mois) return;
|
||||
|
||||
// Réinvestissements triés par date (peuvent être vides)
|
||||
const reinvests = db.prepare(
|
||||
'SELECT montant, date_reinvestissement FROM reinvestissements WHERE investissement_id = ? ORDER BY date_reinvestissement'
|
||||
).all(investissementId);
|
||||
|
||||
const { total_capital } = db.prepare(
|
||||
'SELECT COALESCE(SUM(capital), 0) AS total_capital FROM remboursements WHERE investissement_id = ?'
|
||||
).get(investissementId);
|
||||
|
||||
// Aucun capital remboursé → régénération complète (avec réinvestissements si présents)
|
||||
if (total_capital <= 0) {
|
||||
if (reinvests.length) {
|
||||
generateSimulWithReinvestissements(db, investissementId);
|
||||
} else {
|
||||
generateSimul(db, inv);
|
||||
}
|
||||
adjustFirstPartialPeriod(db, investissementId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Date du dernier remboursement comportant du capital
|
||||
const { last_date } = db.prepare(
|
||||
'SELECT MAX(date_remb) AS last_date FROM remboursements WHERE investissement_id = ? AND capital > 0'
|
||||
).get(investissementId);
|
||||
|
||||
// Capital effectivement investi jusqu'à la date du dernier remboursement
|
||||
// (initial + réinvestissements survenus avant ou à cette date)
|
||||
const capitalAtLastDate = round2(
|
||||
inv.montant_investi +
|
||||
reinvests.filter(r => r.date_reinvestissement <= last_date).reduce((s, r) => s + r.montant, 0)
|
||||
);
|
||||
|
||||
const remainingCapital = round2(capitalAtLastDate - total_capital);
|
||||
|
||||
// Entrées de simulation à recalculer (strictement après la date du dernier remb capital)
|
||||
const futureEntries = db.prepare(`
|
||||
SELECT * FROM simul_remboursements
|
||||
WHERE investissement_id = ? AND date_prevue > ?
|
||||
ORDER BY numero_echeance
|
||||
`).all(investissementId, last_date);
|
||||
|
||||
if (futureEntries.length === 0) return;
|
||||
|
||||
// Capital restant soldé → on met tout à zéro, et on porte le capital sur l'échéance courante
|
||||
if (remainingCapital <= 0) {
|
||||
// Entrée simul qui couvre la période du dernier remboursement capital
|
||||
const lastPaidEntry = db.prepare(`
|
||||
SELECT id, interets_prevus, capital_prevu
|
||||
FROM simul_remboursements
|
||||
WHERE investissement_id = ? AND date_prevue <= ?
|
||||
ORDER BY date_prevue DESC
|
||||
LIMIT 1
|
||||
`).get(investissementId, last_date);
|
||||
|
||||
db.transaction(() => {
|
||||
// Supprimer les échéances futures devenues caduques
|
||||
const stmtDel = db.prepare('DELETE FROM simul_remboursements WHERE id=?');
|
||||
for (const e of futureEntries) stmtDel.run(e.id);
|
||||
|
||||
// Mettre à jour l'échéance courante pour qu'elle reflète le capital soldé
|
||||
if (lastPaidEntry) {
|
||||
db.prepare(`
|
||||
UPDATE simul_remboursements
|
||||
SET capital_prevu=?, total_prevu=?
|
||||
WHERE id=?
|
||||
`).run(
|
||||
capitalAtLastDate,
|
||||
round2(capitalAtLastDate + lastPaidEntry.interets_prevus),
|
||||
lastPaidEntry.id,
|
||||
);
|
||||
}
|
||||
})();
|
||||
return;
|
||||
}
|
||||
|
||||
const step = inv.freq_interets === 'trimestriel' ? 3 : 1;
|
||||
const rPer = (inv.taux_interet / 100 / 12) * step;
|
||||
const nFuture = futureEntries.length;
|
||||
const type = inv.type_remb || 'in_fine';
|
||||
|
||||
// Réinvestissements encore à venir (après la date du dernier remboursement)
|
||||
const futureReinvests = reinvests.filter(r => r.date_reinvestissement > last_date);
|
||||
|
||||
const updates = [];
|
||||
|
||||
if (type === 'differe') {
|
||||
// Versement unique : recalcul sur capital restant + réinvestissements futurs
|
||||
const entry = futureEntries[0];
|
||||
const extraCapital = futureReinvests
|
||||
.filter(r => r.date_reinvestissement <= entry.date_prevue)
|
||||
.reduce((s, r) => s + r.montant, 0);
|
||||
const totalCap = round2(remainingCapital + extraCapital);
|
||||
const moisRestants = monthsDiff(last_date, entry.date_prevue);
|
||||
const interets = round2(totalCap * (inv.taux_interet / 100 / 12) * moisRestants);
|
||||
updates.push({ id: entry.id, capital: totalCap, interets, total: round2(totalCap + interets) });
|
||||
|
||||
} else if (type === 'in_fine') {
|
||||
// Intérêts sur capital courant (augmente à chaque réinvestissement futur)
|
||||
let capital = remainingCapital;
|
||||
let reinvestIdx = 0;
|
||||
futureEntries.forEach((entry, i) => {
|
||||
while (reinvestIdx < futureReinvests.length &&
|
||||
futureReinvests[reinvestIdx].date_reinvestissement <= entry.date_prevue) {
|
||||
capital += futureReinvests[reinvestIdx].montant;
|
||||
reinvestIdx++;
|
||||
}
|
||||
const isLast = i === nFuture - 1;
|
||||
const interets = round2(capital * rPer);
|
||||
updates.push({
|
||||
id: entry.id,
|
||||
capital: isLast ? capital : 0,
|
||||
interets,
|
||||
total: round2(interets + (isLast ? capital : 0)),
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
// Amortissable : recalcul période par période avec capital courant
|
||||
let restant = remainingCapital;
|
||||
let reinvestIdx = 0;
|
||||
futureEntries.forEach((entry, i) => {
|
||||
while (reinvestIdx < futureReinvests.length &&
|
||||
futureReinvests[reinvestIdx].date_reinvestissement <= entry.date_prevue) {
|
||||
restant += futureReinvests[reinvestIdx].montant;
|
||||
reinvestIdx++;
|
||||
}
|
||||
const periodsLeft = nFuture - i;
|
||||
const a = rPer === 0
|
||||
? restant / periodsLeft
|
||||
: restant * rPer / (1 - Math.pow(1 + rPer, -periodsLeft));
|
||||
const interets = round2(restant * rPer);
|
||||
const capital = round2(a - interets);
|
||||
restant = round2(restant - capital);
|
||||
updates.push({ id: entry.id, capital, interets, total: round2(a) });
|
||||
});
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
const stmt = db.prepare(`
|
||||
UPDATE simul_remboursements
|
||||
SET capital_prevu = ?, interets_prevus = ?, total_prevu = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
for (const u of updates) stmt.run(u.capital, u.interets, u.total, u.id);
|
||||
})();
|
||||
|
||||
// Ajustement de la première période partielle (mois incomplet)
|
||||
adjustFirstPartialPeriod(db, investissementId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détecte et corrige la première période partielle (mois incomplet au démarrage du prêt).
|
||||
*
|
||||
* Pour les prêts in_fine et amortissables, la première échéance avec intérêts correspond
|
||||
* parfois à un mois incomplet (souscription en milieu de mois). Si le premier remboursement
|
||||
* réel avec intérêts est inférieur au montant simulé, on met à jour la ligne simul
|
||||
* correspondante avec la valeur réelle et on reporte la différence sur la dernière échéance.
|
||||
*
|
||||
* L'ajustement est idempotent : si les valeurs sont déjà cohérentes, rien n'est modifié.
|
||||
*/
|
||||
function adjustFirstPartialPeriod(db, investissementId) {
|
||||
const inv = db.prepare(
|
||||
'SELECT type_remb FROM investissements WHERE id = ?'
|
||||
).get(investissementId);
|
||||
|
||||
if (!inv || inv.type_remb === 'differe') return;
|
||||
|
||||
// Premier remboursement enregistré avec des intérêts (exclure cashback-only)
|
||||
const firstRemb = db.prepare(`
|
||||
SELECT date_remb, interets_bruts
|
||||
FROM remboursements
|
||||
WHERE investissement_id = ? AND interets_bruts > 0
|
||||
ORDER BY date_remb ASC
|
||||
LIMIT 1
|
||||
`).get(investissementId);
|
||||
|
||||
if (!firstRemb) return;
|
||||
|
||||
const firstRembMonth = firstRemb.date_remb.slice(0, 7); // YYYY-MM
|
||||
|
||||
// Entrée simul du même mois YYYY-MM
|
||||
const simulEntry = db.prepare(`
|
||||
SELECT id, interets_prevus, capital_prevu, total_prevu
|
||||
FROM simul_remboursements
|
||||
WHERE investissement_id = ? AND substr(date_prevue, 1, 7) = ?
|
||||
`).get(investissementId, firstRembMonth);
|
||||
|
||||
if (!simulEntry) return;
|
||||
|
||||
const diff = round2(simulEntry.interets_prevus - firstRemb.interets_bruts);
|
||||
if (diff <= 0.001) return; // Pas d'écart significatif, aucune correction nécessaire
|
||||
|
||||
// Dernière échéance simul (celle qui absorbera la différence)
|
||||
const lastEntry = db.prepare(`
|
||||
SELECT id, interets_prevus, capital_prevu, total_prevu
|
||||
FROM simul_remboursements
|
||||
WHERE investissement_id = ?
|
||||
ORDER BY numero_echeance DESC
|
||||
LIMIT 1
|
||||
`).get(investissementId);
|
||||
|
||||
if (!lastEntry || lastEntry.id === simulEntry.id) return;
|
||||
|
||||
const newFirstInterets = round2(firstRemb.interets_bruts);
|
||||
const newFirstTotal = round2(simulEntry.capital_prevu + newFirstInterets);
|
||||
const newLastInterets = round2(lastEntry.interets_prevus + diff);
|
||||
const newLastTotal = round2(lastEntry.capital_prevu + newLastInterets);
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
'UPDATE simul_remboursements SET interets_prevus=?, total_prevu=? WHERE id=?'
|
||||
).run(newFirstInterets, newFirstTotal, simulEntry.id);
|
||||
db.prepare(
|
||||
'UPDATE simul_remboursements SET interets_prevus=?, total_prevu=? WHERE id=?'
|
||||
).run(newLastInterets, newLastTotal, lastEntry.id);
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère (ou régénère) le tableau d'amortissement en tenant compte des réinvestissements.
|
||||
*
|
||||
* Pour chaque période, le capital pris en compte est :
|
||||
* montant_investi + SUM(reinvestissements dont date <= date_période)
|
||||
*
|
||||
* - in_fine : intérêts recalculés sur le capital cumulé à chaque période,
|
||||
* capital final = capital total cumulé
|
||||
* - differe : versement unique recalculé sur le capital total à l'échéance
|
||||
* - amortissable : intérêts de base + quote-part du réinvestissement sur les périodes restantes
|
||||
*/
|
||||
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
|
||||
FROM investissements WHERE id = ?
|
||||
`).get(investissementId);
|
||||
|
||||
if (!inv) return;
|
||||
|
||||
const reinvests = db.prepare(
|
||||
'SELECT montant, date_reinvestissement FROM reinvestissements WHERE investissement_id = ? ORDER BY date_reinvestissement'
|
||||
).all(investissementId);
|
||||
|
||||
// Pas de réinvestissement → génération standard
|
||||
if (!reinvests.length) {
|
||||
generateSimul(db, inv);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inv.taux_interet || !inv.duree_mois) return;
|
||||
|
||||
const startDate = inv.date_debut_simul || inv.date_premiere_echeance || inv.date_souscription;
|
||||
if (!startDate) return;
|
||||
|
||||
const finDeMois = !!inv.echeance_fin_de_mois;
|
||||
const type = inv.type_remb || 'in_fine';
|
||||
const freq = inv.freq_interets || 'mensuel';
|
||||
const step = freq === 'trimestriel' ? 3 : 1;
|
||||
const rPer = (inv.taux_interet / 100 / 12) * step;
|
||||
|
||||
// Durée effective (tient compte d'une éventuelle restructuration)
|
||||
let effectiveDuree = inv.duree_mois;
|
||||
if (inv.date_debut_simul && inv.date_premiere_echeance && inv.date_debut_simul > inv.date_premiere_echeance) {
|
||||
const elapsed = monthsDiff(inv.date_premiere_echeance, inv.date_debut_simul);
|
||||
effectiveDuree = Math.max(1, inv.duree_mois - elapsed);
|
||||
}
|
||||
|
||||
// Calendrier de base pour obtenir les dates de chaque échéance
|
||||
const baseSchedule = buildSchedule({
|
||||
montant: inv.montant_investi,
|
||||
taux: inv.taux_interet,
|
||||
duree: effectiveDuree,
|
||||
type, freq, startDate, finDeMois,
|
||||
});
|
||||
|
||||
let capital = inv.montant_investi;
|
||||
let reinvestIdx = 0;
|
||||
const schedule = [];
|
||||
|
||||
// Pour amortissable : on garde la structure du capital initial et ajoute l'impact
|
||||
// du réinvestissement sur les périodes restantes (quote-part d'intérêts supplémentaires).
|
||||
// Indexes des réinvestissements non encore traités dans le plan amorti.
|
||||
let pendingReinvests = [];
|
||||
|
||||
for (let i = 0; i < baseSchedule.length; i++) {
|
||||
const entry = baseSchedule[i];
|
||||
|
||||
// Réinvestissements dont la date tombe avant (ou sur) cette échéance
|
||||
while (reinvestIdx < reinvests.length &&
|
||||
reinvests[reinvestIdx].date_reinvestissement <= entry.date) {
|
||||
const r = reinvests[reinvestIdx];
|
||||
capital += r.montant;
|
||||
// Pour amortissable : mémoriser le montant réinvesti et le nb de périodes restantes
|
||||
if (type === 'amortissable') {
|
||||
pendingReinvests.push({ montant: r.montant, remainingPeriods: baseSchedule.length - i });
|
||||
}
|
||||
reinvestIdx++;
|
||||
}
|
||||
|
||||
const isLast = i === baseSchedule.length - 1;
|
||||
|
||||
let entryCapital, entryInterets, entryTotal;
|
||||
|
||||
if (type === 'in_fine') {
|
||||
entryInterets = round2(capital * rPer);
|
||||
entryCapital = isLast ? capital : 0;
|
||||
entryTotal = round2(entryInterets + entryCapital);
|
||||
|
||||
} else if (type === 'differe') {
|
||||
// Versement unique : intérêts recalculés sur le capital total depuis le début
|
||||
const totalMonths = inv.duree_mois;
|
||||
entryInterets = round2(capital * (inv.taux_interet / 100 / 12) * totalMonths);
|
||||
entryCapital = capital;
|
||||
entryTotal = round2(entryCapital + entryInterets);
|
||||
|
||||
} else {
|
||||
// Amortissable : intérêts de base + quote-part des réinvestissements
|
||||
const baseInterets = entry.interets;
|
||||
const extraInterets = pendingReinvests.reduce((sum, pr) => {
|
||||
return sum + round2(pr.montant * rPer);
|
||||
}, 0);
|
||||
entryInterets = round2(baseInterets + extraInterets);
|
||||
entryCapital = entry.capital;
|
||||
entryTotal = round2(entryCapital + entryInterets);
|
||||
}
|
||||
|
||||
schedule.push({ n: entry.n, date: entry.date, capital: entryCapital, interets: entryInterets, total: entryTotal });
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(investissementId);
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO simul_remboursements
|
||||
(investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
`);
|
||||
for (const e of schedule) {
|
||||
stmt.run(investissementId, e.n, e.date, e.capital, e.interets, e.total);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère (ou régénère) le tableau d'amortissement d'un investissement dans la DB.
|
||||
* Ne fait rien si taux_interet ou duree_mois est absent.
|
||||
*
|
||||
* Si date_debut_simul est renseigné (restructuration de prêt), la simulation
|
||||
* démarre à cette date et la durée effective est réduite du nombre de mois déjà
|
||||
* écoulés depuis date_premiere_echeance, afin de ne pas allonger artificiellement le prêt.
|
||||
*/
|
||||
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;
|
||||
|
||||
if (!taux_interet || !duree_mois) return;
|
||||
|
||||
// date_debut_simul remplace le point de départ quand le prêt a été restructuré
|
||||
const startDate = date_debut_simul || date_premiere_echeance || date_souscription;
|
||||
if (!startDate) return;
|
||||
|
||||
// Durée effective : si restructuration, on soustrait les mois déjà écoulés
|
||||
// pour que la simulation se termine bien à la date cible contractuelle d'origine.
|
||||
let effectiveDuree = duree_mois;
|
||||
if (date_debut_simul && date_premiere_echeance && date_debut_simul > date_premiere_echeance) {
|
||||
const elapsed = monthsDiff(date_premiere_echeance, date_debut_simul);
|
||||
effectiveDuree = Math.max(1, duree_mois - elapsed);
|
||||
}
|
||||
|
||||
const echeances = buildSchedule({
|
||||
montant: montant_investi,
|
||||
taux: taux_interet,
|
||||
duree: effectiveDuree,
|
||||
type: type_remb || 'in_fine',
|
||||
freq: freq_interets || 'mensuel',
|
||||
startDate,
|
||||
finDeMois: !!echeance_fin_de_mois,
|
||||
});
|
||||
|
||||
const tx = db.transaction(() => {
|
||||
if (date_debut_simul) {
|
||||
// ── Mode restructuration ──────────────────────────────────────────────
|
||||
// Conserver uniquement les échéances déjà payées avant la date de restructuration
|
||||
// (correspondance par mois YYYY-MM avec les remboursements réels enregistrés).
|
||||
// Les échéances de la période creuse (non payées entre fin de la phase initiale
|
||||
// et date_debut_simul) sont supprimées avec tout ce qui suit.
|
||||
const keptEntries = db.prepare(`
|
||||
SELECT sr.id, sr.numero_echeance FROM simul_remboursements sr
|
||||
WHERE sr.investissement_id = ?
|
||||
AND sr.date_prevue < ?
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM remboursements r
|
||||
WHERE r.investissement_id = sr.investissement_id
|
||||
AND substr(r.date_remb, 1, 7) = substr(sr.date_prevue, 1, 7)
|
||||
)
|
||||
ORDER BY sr.numero_echeance
|
||||
`).all(id, date_debut_simul);
|
||||
|
||||
if (keptEntries.length > 0) {
|
||||
// Supprime tout sauf les entrées payées conservées
|
||||
db.prepare(
|
||||
`DELETE FROM simul_remboursements WHERE investissement_id = ? AND id NOT IN (${keptEntries.map(() => '?').join(',')})`
|
||||
).run(id, ...keptEntries.map(e => e.id));
|
||||
} else {
|
||||
// Rien à conserver → suppression totale
|
||||
db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(id);
|
||||
}
|
||||
|
||||
// Numérotation absolue : position dans le prêt total = mois écoulés depuis la 1ère échéance.
|
||||
// Ex : date_premiere_echeance = août 2024, date_debut_simul = juillet 2025
|
||||
// → 11 mois écoulés → nouvelle échéance 1 = n° 12, dernière = n° 48 (sur 48 total).
|
||||
// On ne se base PAS sur le nombre d'entrées conservées (qui peut différer si certains
|
||||
// paiements in fine ne matchent pas exactement) mais sur le décalage calendaire réel.
|
||||
const elapsedMonths = (date_premiere_echeance && date_debut_simul > date_premiere_echeance)
|
||||
? monthsDiff(date_premiere_echeance, date_debut_simul)
|
||||
: keptEntries.length; // fallback : nombre de lignes conservées
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO simul_remboursements
|
||||
(investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
`);
|
||||
for (const e of echeances) {
|
||||
stmt.run(id, elapsedMonths + e.n, e.date, e.capital, e.interets, e.total);
|
||||
}
|
||||
|
||||
} else {
|
||||
// ── Mode standard : régénération complète ────────────────────────────
|
||||
db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(id);
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO simul_remboursements
|
||||
(investissement_id, numero_echeance, date_prevue, capital_prevu, interets_prevus, total_prevu)
|
||||
VALUES (?,?,?,?,?,?)
|
||||
`);
|
||||
for (const e of echeances) {
|
||||
stmt.run(id, e.n, e.date, e.capital, e.interets, e.total);
|
||||
}
|
||||
}
|
||||
});
|
||||
tx();
|
||||
}
|
||||
Reference in New Issue
Block a user