618 lines
25 KiB
JavaScript
618 lines
25 KiB
JavaScript
/**
|
|
* 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 */
|
|
export 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);
|
|
|
|
// taux_interet peut légitimement valoir 0 (ex. révision suite à un arrêt de production
|
|
// n'engendrant plus d'intérêts) — ne pas confondre avec "non renseigné" (null/undefined).
|
|
if (!inv || inv.taux_interet == null || !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;
|
|
}
|
|
|
|
// taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals)
|
|
if (inv.taux_interet == null || !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(() => {
|
|
if (inv.date_debut_simul) {
|
|
// ── Mode restructuration ──────────────────────────────────────────────
|
|
// Même logique que generateSimul() : conserver les échéances déjà payées avant
|
|
// la date de restructuration, supprimer le reste, et renuméroter à partir du
|
|
// nombre de mois réellement écoulés (pas de repartir à 1, qui ferait disparaître
|
|
// les échéances passées — déjà payées — de la table des projections).
|
|
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(investissementId, inv.date_debut_simul);
|
|
|
|
if (keptEntries.length > 0) {
|
|
db.prepare(
|
|
`DELETE FROM simul_remboursements WHERE investissement_id = ? AND id NOT IN (${keptEntries.map(() => '?').join(',')})`
|
|
).run(investissementId, ...keptEntries.map(e => e.id));
|
|
} else {
|
|
db.prepare('DELETE FROM simul_remboursements WHERE investissement_id=?').run(investissementId);
|
|
}
|
|
|
|
const elapsedMonths = (inv.date_premiere_echeance && inv.date_debut_simul > inv.date_premiere_echeance)
|
|
? monthsDiff(inv.date_premiere_echeance, inv.date_debut_simul)
|
|
: keptEntries.length;
|
|
|
|
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, 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(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;
|
|
|
|
// taux_interet peut légitimement valoir 0 (cf. commentaire dans adjustSimulForActuals)
|
|
if (taux_interet == null || !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();
|
|
}
|