This commit is contained in:
ocroguennec committed 2026-09-18 08:55:38 +02:00
1 parent d5792fb3e2
commit 4cd4cbce71
4 files changed
+79 -12

No files matched your search

+33
View File
@@ -3360,6 +3360,39 @@ if (!invPeValCols.includes('plus_value_latente')) {
db.exec('ALTER TABLE investissements_pe_valorisations ADD COLUMN plus_value_latente REAL');
}
// Migration : capital investissable (18/09/26, demande Olivier — bug remonté : le "capital non
// déployé" d'une valorisation détaillée était calculé sur montant_investi (le montant souscrit
// BRUT), alors qu'une partie a pu être immédiatement prélevée en frais "à la source" à la
// souscription (cf. syncValorisationInitiale, InvestissementsPe.jsx) — le capital réellement
// disponible à déployer est montant_investi − ces frais, jamais le montant brut. Colonne
// nullable, calculée et fournie par le front à chaque sauvegarde du deal (POST/PUT
// /investissements-pe) à partir des frais "à la source" saisis à l'étape "Frais" de la modale ;
// NULL = deal sans frais à la source connus, auquel cas montant_investi fait référence partout
// où capital_investissable est utilisé (cf. investissementsPe.js).
const invPeCols = db.prepare('PRAGMA table_info(investissements_pe)').all().map(c => c.name);
if (!invPeCols.includes('capital_investissable')) {
db.exec('ALTER TABLE investissements_pe ADD COLUMN capital_investissable REAL');
// Backfill : pour tout deal ayant déjà une "Valorisation initiale" auto-générée (créée par
// syncValorisationInitiale AVANT cette migration, donc déjà nette des frais à la source), son
// montant EST la définition de capital_investissable — évite d'obliger Olivier à rouvrir et
// ré-enregistrer chaque deal existant pour que le calcul se corrige. Les libellés doivent
// rester synchronisés avec AUTO_VAL_NOTE / AUTO_VAL_NOTE_LEGACY (InvestissementsPe.jsx).
db.exec(`
UPDATE investissements_pe
SET capital_investissable = (
SELECT v.valorisation_nette FROM investissements_pe_valorisations v
WHERE v.investissement_pe_id = investissements_pe.id
AND v.notes IN ('Valorisation initiale (souscription)', 'Valorisation initiale (frais de souscription)')
ORDER BY v.date_valorisation ASC, v.id ASC LIMIT 1
)
WHERE EXISTS (
SELECT 1 FROM investissements_pe_valorisations v
WHERE v.investissement_pe_id = investissements_pe.id
AND v.notes IN ('Valorisation initiale (souscription)', 'Valorisation initiale (frais de souscription)')
)
`);
}
// Lien optionnel remboursement → deal PE (17/09/26) : une distribution (retour
// de capital, plus-value) perçue sur un deal PE est saisie comme un
// remboursement bonus_plateforme/interets_plateforme, comme demandé par
+22 -9
View File
@@ -29,6 +29,13 @@ const Schema = z.object({
date_souscription: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
duree_mois: z.number().int().positive().nullable().optional(),
montant_investi: z.number().positive(),
// Capital réellement disponible à déployer, net des frais prélevés "à la source" à la
// souscription (18/09/26, demande Olivier — cf. migration capital_investissable dans
// db/index.js). Calculé et fourni par le front (InvestissementsPe.jsx / InvestissementPeDetail
// .jsx) à partir de l'étape "Frais" de la modale ; NULL si inconnu (deal sans frais à la
// source, ou pas encore resauvegardé depuis cette fonctionnalité) — auquel cas montant_investi
// sert de repli partout où capital_investissable est utilisé.
capital_investissable: z.number().nonnegative().nullable().optional(),
objectif: z.string().optional(),
statut: z.enum(['en_attente', 'valide', 'cloture', 'perte_definitive']).default('valide'),
reference: z.string().optional(),
@@ -170,11 +177,12 @@ router.post('/', requireInvestisseur, (req, res, next) => {
const r = db.prepare(`
INSERT INTO investissements_pe
(investisseur_id, plateforme_id, workspace_id, nom_deal, strategie, date_souscription,
duree_mois, montant_investi, objectif, statut, reference, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
duree_mois, montant_investi, capital_investissable, objectif, statut, reference, notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
`).run(
investisseurId, body.plateforme_id, workspaceId, body.nom_deal, body.strategie || null,
body.date_souscription, body.duree_mois ?? null, body.montant_investi, body.objectif || null,
body.date_souscription, body.duree_mois ?? null, body.montant_investi,
body.capital_investissable ?? null, body.objectif || null,
body.statut, body.reference || null, body.notes || null,
);
res.status(201).json({ id: r.lastInsertRowid, ...body });
@@ -227,13 +235,13 @@ router.put('/:id', requireInvestisseur, (req, res, next) => {
db.prepare(`
UPDATE investissements_pe
SET investisseur_id=?, plateforme_id=?, nom_deal=?, strategie=?, date_souscription=?,
duree_mois=?, montant_investi=?, objectif=?, statut=?, reference=?, notes=?,
updated_at=datetime('now')
duree_mois=?, montant_investi=?, capital_investissable=?, objectif=?, statut=?,
reference=?, notes=?, updated_at=datetime('now')
WHERE id=?
`).run(
investisseurId, body.plateforme_id, body.nom_deal, body.strategie || null, body.date_souscription,
body.duree_mois ?? null, body.montant_investi, body.objectif || null, body.statut,
body.reference || null, body.notes || null, id,
body.duree_mois ?? null, body.montant_investi, body.capital_investissable ?? null,
body.objectif || null, body.statut, body.reference || null, body.notes || null, id,
);
res.json({ id, ...body });
} catch (e) { next(e); }
@@ -275,16 +283,21 @@ router.post('/:id/valorisations', requireInvestisseur, (req, res, next) => {
// Quand la répartition est renseignée (capital_deploye présent), la valorisation nette est
// recalculée ici plutôt que reprise du body : capital_deploye + plus_value_latente +
// capital_non_deploye (déduit = montant_investi du deal − capital_deploye, jamais négatif).
// capital_non_deploye (déduit = capital investissable du deal − capital_deploye, jamais
// négatif). Capital investissable = montant_investi net des frais "à la source" prélevés à
// la souscription quand ils sont connus (deal.capital_investissable), sinon montant_investi
// brut par repli (18/09/26, correctif Olivier — jusque-là on utilisait toujours le montant
// brut, ce qui gonflait à tort le capital non déployé pour un deal avec frais à la source).
// Sans répartition (mode simple, historique), comportement inchangé : le montant saisi est
// utilisé tel quel et les deux colonnes restent NULL.
let valorisationNette = body.valorisation_nette;
let capitalDeploye = null;
let plusValueLatente = null;
if (body.capital_deploye != null) {
const capitalInvestissable = deal.capital_investissable ?? deal.montant_investi;
capitalDeploye = body.capital_deploye;
plusValueLatente = body.plus_value_latente ?? 0;
const capitalNonDeploye = Math.max(0, deal.montant_investi - capitalDeploye);
const capitalNonDeploye = Math.max(0, capitalInvestissable - capitalDeploye);
valorisationNette = Math.round((capitalDeploye + plusValueLatente + capitalNonDeploye) * 100) / 100;
}