MAJ
This commit is contained in:
1 parent
6bba34396c
commit
1d5737703b
9 files changed
+347
-45
No files matched your search
@@ -181,6 +181,159 @@ router.get('/valorisations', (req, res) => {
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/investissements-pe/mouvements-portefeuille — 19/09/26, demande Olivier : "le suivi
|
||||
* de mouvement du porte-monnaie doit être commun [crowdlending + PE], avec la possibilité de
|
||||
* distinguer si l'on parle de crowdlending ou de PE".
|
||||
*
|
||||
* Contexte du bug signalé : l'onglet "Mouvements Porte-monnaie" (Plateformes.jsx, calcul du
|
||||
* solde indicatif ligne à ligne via MouvementsShared.computeSoldeIndicatifMap) ne consomme que
|
||||
* des endpoints bornés au workspace actif (GET /remboursements, /depots-retraits,
|
||||
* /frais-operations, /investissements) — cohérent pour séparer la performance crowdlending de
|
||||
* la performance PE, mais faux pour le porte-monnaie : sur une plateforme partagée (ex.
|
||||
* Fundora), l'argent est UN SEUL porte-monnaie physique chez le courtier, alimenté ou ponctionné
|
||||
* indifféremment par les deux activités. Un remboursement PE réutilisé pour souscrire un deal
|
||||
* crowdlending devient alors invisible du côté crowdlending, et le cumul plonge artificiellement
|
||||
* dans le négatif après chaque retrait — exactement le symptôme observé sur Fundora (3,45 €).
|
||||
*
|
||||
* Même principe déjà retenu pour solde_portefeuille (dashboard.js / dashboardPe.js, walletMap,
|
||||
* 17/09/26) : cet endpoint-ci fournit la partie du porte-monnaie qui vit HORS du workspace
|
||||
* actuellement actif (typiquement PE, quand appelé depuis Plateformes.jsx en workspace
|
||||
* crowdlending), pour que le frontend la fusionne avec ce qu'il a déjà côté workspace actif —
|
||||
* sans double-compte, et sans avoir à assouplir le scope workspace_id des endpoints existants.
|
||||
* Quatre familles de lignes, déjà mises en forme proche de MouvementsShared (type/montant/
|
||||
* date_operation/plateforme_id/libelle) pour limiter le travail de fusion côté frontend :
|
||||
* - souscription : capital déployé à la souscription de chaque deal PE (montant_investi),
|
||||
* pas encore modélisé comme mouvement ailleurs (pendant PE de la ligne "souscription"
|
||||
* synthétique que Plateformes.jsx génère déjà pour le crowdlending).
|
||||
* - interets_plateforme : distributions PE (remboursements.type='interets_plateforme'),
|
||||
* créditées pour leur net_recu — même convention que interetsPlateformeWalletPerPlat dans
|
||||
* dashboardPe.js ("net_recu tient déjà compte des prélèvements").
|
||||
* - frais : frais_operations rattachés à investissement_pe_id, jusqu'ici absents aussi du
|
||||
* calcul officiel du solde (cf. correctif fraisPerPlat du même jour dans dashboard*.js).
|
||||
* - depot / retrait : dépôts/retraits manuels (table depots_retraits) enregistrés alors que
|
||||
* le workspace PE était actif — AJOUTÉ le 19/09/26 suite au signalement Olivier sur Fundora,
|
||||
* où 4 dépôts manuels de 800 € (un par deal PE) étaient invisibles du porte-monnaie
|
||||
* crowdlending, expliquant l'essentiel du solde indicatif faussé (bien plus que les 3,45 €
|
||||
* de distribution initialement en cause). Filtrées sur workspace_id != workspace appelant
|
||||
* (résolu comme à l'écriture, cf. resolveActiveWorkspaceId) plutôt que sur un id "PE" en
|
||||
* dur, pour rester correct si un 3e workspace apparaît un jour. `raw_source` porte la valeur
|
||||
* brute de depots_retraits.source (manuel/import_excel/auto_remboursement) pour que le
|
||||
* frontend puisse reconstituer le badge "AUTO" comme pour les dépôts crowdlending.
|
||||
*
|
||||
* corrections_solde n'a PAS de colonne workspace_id (vérifié en base) : aucune correction ne
|
||||
* peut donc être "perdue" par ce bug, ce n'était pas un vrai gap malgré ce qu'un premier passage
|
||||
* avait supposé par prudence.
|
||||
*/
|
||||
router.get('/mouvements-portefeuille', (req, res) => {
|
||||
const scopeAll = req.query.scope === 'all';
|
||||
const userId = req.user.id;
|
||||
|
||||
let invCond, invArgs, invCondDr, invArgsDr;
|
||||
if (scopeAll) {
|
||||
invCond = 'inv.user_id = ?';
|
||||
invArgs = [userId];
|
||||
invCondDr = 'dr.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
|
||||
invArgsDr = [userId];
|
||||
} else {
|
||||
const raw = req.header('X-Investisseur-Id');
|
||||
const id = Number(raw);
|
||||
if (!id) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
|
||||
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(id, userId);
|
||||
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
|
||||
invCond = 'ipe.investisseur_id = ?';
|
||||
invArgs = [id];
|
||||
invCondDr = 'dr.investisseur_id = ?';
|
||||
invArgsDr = [id];
|
||||
}
|
||||
|
||||
// Volontairement PAS de filtre workspace_id sur souscriptions/distributions/frais (tables
|
||||
// dédiées PE) : ce sont par construction des lignes qui vivent hors du workspace actif
|
||||
// (crowdlending) qui appelle cet endpoint. Pour depots_retraits en revanche (table partagée
|
||||
// entre workspaces), il FAUT exclure explicitement le workspace appelant, pour ne pas
|
||||
// dupliquer ce que le frontend a déjà récupéré via son propre GET /depots-retraits scopé.
|
||||
const callerWorkspaceId = resolveActiveWorkspaceId(req);
|
||||
|
||||
const souscriptions = db.prepare(`
|
||||
SELECT ipe.id AS deal_id, ipe.plateforme_id, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
ipe.date_souscription AS date_operation, ipe.montant_investi AS montant,
|
||||
ipe.nom_deal AS libelle, ipe.investisseur_id, inv.nom AS investisseur_nom
|
||||
FROM investissements_pe ipe
|
||||
JOIN plateformes p ON p.id = ipe.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
JOIN investisseurs inv ON inv.id = ipe.investisseur_id
|
||||
WHERE ${invCond}
|
||||
`).all(...invArgs);
|
||||
|
||||
const distributions = db.prepare(`
|
||||
SELECT r.id AS remb_id, ipe.plateforme_id, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
r.date_remb AS date_operation, r.net_recu AS montant,
|
||||
ipe.nom_deal AS libelle, ipe.investisseur_id, inv.nom AS investisseur_nom
|
||||
FROM remboursements r
|
||||
JOIN investissements_pe ipe ON ipe.id = r.investissement_pe_id
|
||||
JOIN plateformes p ON p.id = ipe.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
JOIN investisseurs inv ON inv.id = ipe.investisseur_id
|
||||
WHERE r.type = 'interets_plateforme' AND ${invCond}
|
||||
`).all(...invArgs);
|
||||
|
||||
const frais = db.prepare(`
|
||||
SELECT f.id AS frais_id, f.plateforme_id, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
f.date_operation, f.montant, f.mode_reglement,
|
||||
ipe.nom_deal AS libelle, ipe.investisseur_id, inv.nom AS investisseur_nom
|
||||
FROM frais_operations f
|
||||
JOIN investissements_pe ipe ON ipe.id = f.investissement_pe_id
|
||||
JOIN plateformes p ON p.id = f.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
JOIN investisseurs inv ON inv.id = ipe.investisseur_id
|
||||
WHERE ${invCond}
|
||||
`).all(...invArgs);
|
||||
|
||||
const depots = db.prepare(`
|
||||
SELECT dr.id AS dr_id, dr.plateforme_id, p.nom AS plateforme_nom,
|
||||
plat_inv.nom AS plateforme_detenteur_nom,
|
||||
dr.date_operation, dr.type, dr.montant, dr.source AS raw_source,
|
||||
dr.libelle, dr.investisseur_id, inv.nom AS investisseur_nom
|
||||
FROM depots_retraits dr
|
||||
JOIN plateformes p ON p.id = dr.plateforme_id
|
||||
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
|
||||
JOIN investisseurs inv ON inv.id = dr.investisseur_id
|
||||
WHERE dr.workspace_id != ? AND ${invCondDr}
|
||||
`).all(callerWorkspaceId, ...invArgsDr);
|
||||
|
||||
const rows = [
|
||||
...souscriptions.map(r => ({
|
||||
id: `pe_sub_${r.deal_id}`, type: 'souscription', montant: r.montant,
|
||||
date_operation: r.date_operation, plateforme_id: r.plateforme_id, plateforme_nom: r.plateforme_nom,
|
||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom, libelle: r.libelle,
|
||||
investisseur_id: r.investisseur_id, investisseur_nom: r.investisseur_nom,
|
||||
})),
|
||||
...distributions.map(r => ({
|
||||
id: `pe_remb_${r.remb_id}`, type: 'interets_plateforme', montant: r.montant,
|
||||
date_operation: r.date_operation, plateforme_id: r.plateforme_id, plateforme_nom: r.plateforme_nom,
|
||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom, libelle: r.libelle,
|
||||
investisseur_id: r.investisseur_id, investisseur_nom: r.investisseur_nom,
|
||||
})),
|
||||
...frais.map(r => ({
|
||||
id: `pe_frais_${r.frais_id}`, type: 'frais', montant: r.montant, mode_reglement: r.mode_reglement,
|
||||
date_operation: r.date_operation, plateforme_id: r.plateforme_id, plateforme_nom: r.plateforme_nom,
|
||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom, libelle: r.libelle,
|
||||
investisseur_id: r.investisseur_id, investisseur_nom: r.investisseur_nom,
|
||||
})),
|
||||
...depots.map(r => ({
|
||||
id: `pe_dr_${r.dr_id}`, type: r.type, montant: r.montant, raw_source: r.raw_source,
|
||||
date_operation: r.date_operation, plateforme_id: r.plateforme_id, plateforme_nom: r.plateforme_nom,
|
||||
plateforme_detenteur_nom: r.plateforme_detenteur_nom, libelle: r.libelle,
|
||||
investisseur_id: r.investisseur_id, investisseur_nom: r.investisseur_nom,
|
||||
})),
|
||||
];
|
||||
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
router.post('/', requireInvestisseur, (req, res, next) => {
|
||||
try {
|
||||
const body = Schema.parse(req.body);
|
||||
|
||||
Reference in new issue
Block a user