MAj API pour PE
This commit is contained in:
1 parent
c918de1e7d
commit
8817ac693d
7 files changed
+377
-25
No files matched your search
@@ -24,6 +24,14 @@ const round2 = v => Math.round((v ?? 0) * 100) / 100;
|
||||
* `gain_net_depuis_debut.frais_hors_remboursement`. Sur tout l'historique du portefeuille —
|
||||
* reste valable même après un retrait complet des plateformes, contrairement à une approche
|
||||
* basée sur le solde courant.
|
||||
*
|
||||
* Tous les champs ci-dessus (`investissements`, `interets`, `cash`, `gain_net_depuis_debut`)
|
||||
* portent UNIQUEMENT sur le crowdlending. `investissements_pe` (20/09/26) est un objet
|
||||
* séparé qui résume le portefeuille Private Equity avec ses propres formules (dernière
|
||||
* valorisation connue par deal, distributions perçues, TVPI/DPI/RVPI) — mêmes calculs que
|
||||
* la page Tableau de bord PE de l'application. Volontairement pas fusionné dans les champs
|
||||
* crowdlending ci-dessus : les deux modèles ne sont pas comparables terme à terme (un deal
|
||||
* PE n'a ni statut "en retard"/"procédure" ni capital restant dû amortissable).
|
||||
* tags: [Dashboard]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
@@ -149,6 +157,46 @@ router.get('/', (req, res) => {
|
||||
- fraisHorsRembRow.total
|
||||
);
|
||||
|
||||
// ── Section Private Equity (20/09/26, demande Olivier — améliorer l'API/MCP pour couvrir le
|
||||
// PE) : mêmes formules que les KPIs de la page Tableau de bord PE de l'app (DashboardPe.jsx,
|
||||
// useMemo `totals`) — investi = somme montant_investi (tous deals, actifs et soldés) ; nav =
|
||||
// dernière valorisation connue de chaque deal, ou montant_investi par repli si aucune
|
||||
// valorisation saisie ; distributions = somme des distributions cumulées perçues ; tvpi/dpi/
|
||||
// rvpi dérivés. Objet séparé de `investissements` ci-dessus plutôt que fusionné — voir la
|
||||
// note dans la description OpenAPI au-dessus de cette route.
|
||||
const invPeCond = req.investisseurScopeAll
|
||||
? 'ipe.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'ipe.investisseur_id = ?';
|
||||
const invPeRows = db.prepare(`
|
||||
SELECT ipe.montant_investi, ipe.statut,
|
||||
(SELECT v.valorisation_nette FROM investissements_pe_valorisations v
|
||||
WHERE v.investissement_pe_id = ipe.id
|
||||
ORDER BY v.date_valorisation DESC, v.id DESC LIMIT 1) AS derniere_valorisation,
|
||||
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
|
||||
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees
|
||||
FROM investissements_pe ipe
|
||||
WHERE ${invPeCond}
|
||||
`).all(invParam);
|
||||
|
||||
let peInvesti = 0, peNav = 0, peDistributions = 0, peActifs = 0, peSoldes = 0;
|
||||
for (const r of invPeRows) {
|
||||
peInvesti += r.montant_investi;
|
||||
peNav += (r.derniere_valorisation ?? r.montant_investi);
|
||||
peDistributions += r.distributions_cumulees;
|
||||
if (r.statut === 'valide' || r.statut === 'en_attente') peActifs++; else peSoldes++;
|
||||
}
|
||||
const investissements_pe = {
|
||||
nb_deals: invPeRows.length,
|
||||
nb_actifs: peActifs,
|
||||
nb_soldes: peSoldes,
|
||||
total_investi: round2(peInvesti),
|
||||
valorisation_totale: round2(peNav),
|
||||
distributions_cumulees: round2(peDistributions),
|
||||
tvpi: peInvesti > 0 ? round2((peNav + peDistributions) / peInvesti) : null,
|
||||
dpi: peInvesti > 0 ? round2(peDistributions / peInvesti) : null,
|
||||
rvpi: peInvesti > 0 ? round2(peNav / peInvesti) : null,
|
||||
};
|
||||
|
||||
res.json({
|
||||
investissements,
|
||||
interets: { ...interets, annee: annee || null },
|
||||
@@ -160,6 +208,7 @@ router.get('/', (req, res) => {
|
||||
corrections: round2(correctionsRow.total),
|
||||
frais_hors_remboursement: round2(fraisHorsRembRow.total),
|
||||
},
|
||||
investissements_pe,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const router = Router();
|
||||
* @openapi
|
||||
* /frais-operations:
|
||||
* get:
|
||||
* summary: Liste des frais liés aux investissements (entrée, gestion, distribution)
|
||||
* summary: Liste des frais liés aux investissements (crowdlending et Private Equity)
|
||||
* description: >
|
||||
* Frais facturés par certaines plateformes en dehors des intérêts/prélèvements fiscaux
|
||||
* habituels : frais d'entrée à la souscription, frais de gestion, frais sur distribution
|
||||
@@ -21,6 +21,14 @@ const router = Router();
|
||||
* fois. Tous les autres modes ("source", "portefeuille", "compte_courant")
|
||||
* représentent chacun une sortie d'argent réelle (c'est cet ensemble — tout sauf
|
||||
* "remboursement" — qui est déduit de `gain_net_depuis_debut` sur /dashboard).
|
||||
*
|
||||
* Depuis le 20/09/26, couvre aussi bien les frais crowdlending que les frais Private
|
||||
* Equity (auparavant absents de cette route, bien que déjà présents en base) —
|
||||
* `source_type` ("crowdlending" ou "private_equity") indique de quel côté vient chaque
|
||||
* ligne. Une ligne crowdlending porte `investissement_id`/`nom_projet` (et
|
||||
* `investissement_pe_id`/`nom_deal` valent `null`) ; une ligne PE porte l'inverse.
|
||||
* `?workspace=<slug>` permet d'isoler un seul des deux univers ; sans ce paramètre, les
|
||||
* deux sont renvoyés ensemble, triés par date.
|
||||
* tags: [Frais]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
@@ -30,31 +38,91 @@ const router = Router();
|
||||
* - in: query
|
||||
* name: date_fin
|
||||
* schema: { type: string, format: date }
|
||||
* - in: query
|
||||
* name: workspace
|
||||
* schema: { type: string }
|
||||
* description: >
|
||||
* Slug du workspace à filtrer (ex. "crowdlending" ou "private-equity"). Absent par
|
||||
* défaut : renvoie les deux univers confondus — 400 si le slug est inconnu.
|
||||
* responses:
|
||||
* 200: { description: Liste des frais }
|
||||
* 400: { description: Slug de workspace inconnu }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { date_debut, date_fin } = req.query;
|
||||
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du
|
||||
// foyer ; clé mono-investisseur → filtre sur req.investisseurId.
|
||||
const conds = [req.investisseurScopeAll
|
||||
const invParam = req.investisseurScopeAll ? req.userId : req.investisseurId;
|
||||
|
||||
let workspaceFilter = null;
|
||||
if (req.query.workspace) {
|
||||
const ws = db.prepare('SELECT id, type FROM workspaces WHERE slug = ?').get(String(req.query.workspace));
|
||||
if (!ws) return res.status(400).json({ error: `Workspace inconnu : "${req.query.workspace}"` });
|
||||
workspaceFilter = ws;
|
||||
}
|
||||
|
||||
const clConds = [req.investisseurScopeAll
|
||||
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'i.investisseur_id = ?'];
|
||||
const args = [req.investisseurScopeAll ? req.userId : req.investisseurId];
|
||||
if (date_debut) { conds.push('f.date_operation >= ?'); args.push(date_debut); }
|
||||
if (date_fin) { conds.push('f.date_operation <= ?'); args.push(date_fin); }
|
||||
const clArgs = [invParam];
|
||||
if (date_debut) { clConds.push('f.date_operation >= ?'); clArgs.push(date_debut); }
|
||||
if (date_fin) { clConds.push('f.date_operation <= ?'); clArgs.push(date_fin); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT f.id, i.id AS investissement_id, i.nom_projet, p.nom AS plateforme_nom,
|
||||
f.montant, f.mode_reglement, f.categorie, f.date_operation,
|
||||
f.remboursement_id, c.nom AS compte_nom, f.notes
|
||||
const peConds = [req.investisseurScopeAll
|
||||
? 'ipe.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'ipe.investisseur_id = ?'];
|
||||
const peArgs = [invParam];
|
||||
if (date_debut) { peConds.push('f.date_operation >= ?'); peArgs.push(date_debut); }
|
||||
if (date_fin) { peConds.push('f.date_operation <= ?'); peArgs.push(date_fin); }
|
||||
// Le côté crowdlending n'a pas besoin d'un filtre workspace_id explicite : la table
|
||||
// `investissements` n'a pas de colonne workspace_id, il n'existe qu'UN SEUL workspace de type
|
||||
// 'crowdlending' (singleton imposé en base, cf. db/index.js) et toute ligne crowdlending lui
|
||||
// appartient donc implicitement. Côté PE, plusieurs workspaces de type 'private_equity' sont
|
||||
// en revanche possibles (même contrainte) — d'où le filtre explicite ici.
|
||||
if (workspaceFilter?.type === 'private_equity') {
|
||||
peConds.push('ipe.workspace_id = ?');
|
||||
peArgs.push(workspaceFilter.id);
|
||||
}
|
||||
|
||||
const CL_SELECT = `
|
||||
SELECT 'crowdlending' AS source_type,
|
||||
f.id, f.montant, f.mode_reglement, f.categorie, f.date_operation, f.remboursement_id,
|
||||
c.nom AS compte_nom, f.notes,
|
||||
i.id AS investissement_id, i.nom_projet, p.nom AS plateforme_nom,
|
||||
NULL AS investissement_pe_id, NULL AS nom_deal
|
||||
FROM frais_operations f
|
||||
JOIN investissements i ON i.id = f.investissement_id
|
||||
JOIN plateformes p ON p.id = i.plateforme_id
|
||||
LEFT JOIN comptes c ON c.id = f.compte_id
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY f.date_operation DESC, f.id DESC
|
||||
`).all(...args);
|
||||
WHERE ${clConds.join(' AND ')}
|
||||
`;
|
||||
// Même ordre de colonnes que CL_SELECT ci-dessus, position par position : UNION ALL associe
|
||||
// les colonnes par POSITION (pas par nom) — un ordre différent mélangerait silencieusement les
|
||||
// valeurs (ex. plateforme_nom hérité à la place de nom_deal). Repéré et corrigé lors de la
|
||||
// vérification par dry-run avant livraison (20/09/26).
|
||||
const PE_SELECT = `
|
||||
SELECT 'private_equity' AS source_type,
|
||||
f.id, f.montant, f.mode_reglement, f.categorie, f.date_operation, f.remboursement_id,
|
||||
c.nom AS compte_nom, f.notes,
|
||||
NULL AS investissement_id, NULL AS nom_projet, p.nom AS plateforme_nom,
|
||||
ipe.id AS investissement_pe_id, ipe.nom_deal
|
||||
FROM frais_operations f
|
||||
JOIN investissements_pe ipe ON ipe.id = f.investissement_pe_id
|
||||
JOIN plateformes p ON p.id = ipe.plateforme_id
|
||||
LEFT JOIN comptes c ON c.id = f.compte_id
|
||||
WHERE ${peConds.join(' AND ')}
|
||||
`;
|
||||
|
||||
let rows;
|
||||
if (workspaceFilter?.type === 'crowdlending') {
|
||||
rows = db.prepare(`${CL_SELECT} ORDER BY f.date_operation DESC, f.id DESC`).all(...clArgs);
|
||||
} else if (workspaceFilter?.type === 'private_equity') {
|
||||
rows = db.prepare(`${PE_SELECT} ORDER BY f.date_operation DESC, f.id DESC`).all(...peArgs);
|
||||
} else {
|
||||
rows = db.prepare(`
|
||||
SELECT * FROM (${CL_SELECT} UNION ALL ${PE_SELECT})
|
||||
ORDER BY date_operation DESC, id DESC
|
||||
`).all(...clArgs, ...peArgs);
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import investisseurRouter from './investisseur.js';
|
||||
import investissementsRouter from './investissements.js';
|
||||
import investissementsPeRouter from './investissementsPe.js';
|
||||
import remboursementsRouter from './remboursements.js';
|
||||
import depotsRetraitsRouter from './depotsRetraits.js';
|
||||
import fraisOperationsRouter from './fraisOperations.js';
|
||||
@@ -12,6 +13,7 @@ const router = Router();
|
||||
|
||||
router.use('/investisseur', investisseurRouter);
|
||||
router.use('/investissements', investissementsRouter);
|
||||
router.use('/investissements-pe', investissementsPeRouter);
|
||||
router.use('/remboursements', remboursementsRouter);
|
||||
router.use('/depots-retraits', depotsRetraitsRouter);
|
||||
router.use('/frais-operations', fraisOperationsRouter);
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Router } from 'express';
|
||||
import db from '../../db/index.js';
|
||||
import { HttpError } from '../../middleware/errorHandler.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Mêmes colonnes enrichies que GET /api/investissements-pe (route interne,
|
||||
// investissementsPe.js) : dernière valorisation connue, distributions cumulées perçues
|
||||
// (remboursements liés), et le TVPI qui en découle — même formule que la fiche détail interne
|
||||
// et que DashboardPe.jsx (nav = dernière valorisation, ou montant_investi si aucune
|
||||
// valorisation saisie). Colonnes et FROM/JOIN séparés (plutôt qu'un seul bloc SELECT...FROM) pour
|
||||
// que la route détail puisse insérer `ipe.notes` dans la liste de colonnes SANS le faire atterrir
|
||||
// après le FROM/JOIN, ce qui produirait du SQL invalide (repéré et corrigé lors de la
|
||||
// vérification par dry-run avant livraison, 20/09/26).
|
||||
const ENRICHED_COLUMNS = `
|
||||
ipe.id, ipe.nom_deal, ipe.strategie, p.nom AS plateforme_nom,
|
||||
ipe.date_souscription, ipe.duree_mois, ipe.montant_investi, ipe.capital_investissable,
|
||||
ipe.objectif, ipe.statut, ipe.reference,
|
||||
(SELECT v.valorisation_nette FROM investissements_pe_valorisations v
|
||||
WHERE v.investissement_pe_id = ipe.id
|
||||
ORDER BY v.date_valorisation DESC, v.id DESC LIMIT 1) AS derniere_valorisation,
|
||||
(SELECT v.date_valorisation FROM investissements_pe_valorisations v
|
||||
WHERE v.investissement_pe_id = ipe.id
|
||||
ORDER BY v.date_valorisation DESC, v.id DESC LIMIT 1) AS derniere_valorisation_date,
|
||||
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
|
||||
WHERE r.investissement_pe_id = ipe.id), 0) AS distributions_cumulees,
|
||||
COALESCE((SELECT SUM(r.net_recu) FROM remboursements r
|
||||
WHERE r.investissement_pe_id = ipe.id AND r.distribution_nature = 'interet_non_deploye'), 0)
|
||||
AS obligation_cumulee
|
||||
`;
|
||||
const ENRICHED_FROM = `
|
||||
FROM investissements_pe ipe
|
||||
JOIN plateformes p ON p.id = ipe.plateforme_id
|
||||
`;
|
||||
|
||||
function withTvpi(row) {
|
||||
return {
|
||||
...row,
|
||||
tvpi: row.derniere_valorisation != null
|
||||
? Math.round(((row.derniere_valorisation + row.distributions_cumulees) / row.montant_investi) * 100) / 100
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /investissements-pe:
|
||||
* get:
|
||||
* summary: Liste des investissements Private Equity de l'investisseur
|
||||
* description: >
|
||||
* Distinct de /investissements (crowdlending) : un deal PE n'a ni échéancier connu à
|
||||
* l'avance ni taux d'intérêt fixe — un montant unique investi (`montant_investi`), valorisé
|
||||
* périodiquement (`derniere_valorisation`, `derniere_valorisation_date`), avec des
|
||||
* distributions perçues au fil du temps (`distributions_cumulees`, un sous-ensemble marqué
|
||||
* `obligation_cumulee` quand il s'agit d'intérêts sur capital non encore déployé).
|
||||
* `tvpi` (Total Value to Paid-In = (dernière valorisation + distributions cumulées) /
|
||||
* montant investi) est `null` tant qu'aucune valorisation n'a été saisie pour ce deal —
|
||||
* mêmes formules que la page Investissements PE / le tableau de bord PE de l'application.
|
||||
* tags: [Investissements PE]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: statut
|
||||
* schema: { type: string, enum: [en_attente, valide, cloture, perte_definitive] }
|
||||
* - in: query
|
||||
* name: workspace
|
||||
* schema: { type: string }
|
||||
* description: >
|
||||
* Slug du workspace à filtrer (utile si plusieurs workspaces Private Equity existent).
|
||||
* Absent par défaut : tous les deals PE de l'investisseur, tous workspaces confondus.
|
||||
* responses:
|
||||
* 200: { description: Liste des investissements PE }
|
||||
* 400: { description: Slug de workspace inconnu }
|
||||
*/
|
||||
router.get('/', (req, res) => {
|
||||
const { statut } = req.query;
|
||||
const conds = [req.investisseurScopeAll
|
||||
? 'ipe.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'ipe.investisseur_id = ?'];
|
||||
const args = [req.investisseurScopeAll ? req.userId : req.investisseurId];
|
||||
if (statut) { conds.push('ipe.statut = ?'); args.push(statut); }
|
||||
|
||||
if (req.query.workspace) {
|
||||
const ws = db.prepare('SELECT id FROM workspaces WHERE slug = ?').get(String(req.query.workspace));
|
||||
if (!ws) return res.status(400).json({ error: `Workspace inconnu : "${req.query.workspace}"` });
|
||||
conds.push('ipe.workspace_id = ?');
|
||||
args.push(ws.id);
|
||||
}
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT ${ENRICHED_COLUMNS}
|
||||
${ENRICHED_FROM}
|
||||
WHERE ${conds.join(' AND ')}
|
||||
ORDER BY ipe.date_souscription DESC, ipe.id DESC
|
||||
`).all(...args);
|
||||
|
||||
res.json(rows.map(withTvpi));
|
||||
});
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /investissements-pe/{id}:
|
||||
* get:
|
||||
* summary: Détail d'un investissement PE, avec son historique de valorisations
|
||||
* description: >
|
||||
* `valorisations` est l'historique complet des valorisations saisies pour ce deal (la plus
|
||||
* récente de cette liste est celle utilisée pour `derniere_valorisation`/`tvpi`).
|
||||
* `remboursements` liste les distributions perçues (retour de capital, plus-value —
|
||||
* saisies comme des remboursements de type "intérêts plateforme", cf.
|
||||
* /remboursements) ; `frais_operations` liste les frais rattachés à ce deal, quel que
|
||||
* soit leur mode de règlement.
|
||||
* tags: [Investissements PE]
|
||||
* security: [{ ApiKeyAuth: [] }]
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema: { type: integer }
|
||||
* responses:
|
||||
* 200: { description: Détail de l'investissement PE }
|
||||
* 404: { description: Investissement PE introuvable }
|
||||
*/
|
||||
router.get('/:id', (req, res, next) => {
|
||||
try {
|
||||
const invCond = req.investisseurScopeAll
|
||||
? 'ipe.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
|
||||
: 'ipe.investisseur_id = ?';
|
||||
const invParam = req.investisseurScopeAll ? req.userId : req.investisseurId;
|
||||
|
||||
const row = db.prepare(`
|
||||
SELECT ${ENRICHED_COLUMNS}, ipe.notes
|
||||
${ENRICHED_FROM}
|
||||
WHERE ipe.id = ? AND ${invCond}
|
||||
`).get(req.params.id, invParam);
|
||||
if (!row) throw new HttpError(404, 'Investissement PE introuvable');
|
||||
|
||||
const valorisations = db.prepare(`
|
||||
SELECT id, date_valorisation, valorisation_nette, capital_deploye, plus_value_latente, notes
|
||||
FROM investissements_pe_valorisations
|
||||
WHERE investissement_pe_id = ?
|
||||
ORDER BY date_valorisation DESC, id DESC
|
||||
`).all(row.id);
|
||||
|
||||
const remboursements = db.prepare(`
|
||||
SELECT id, date_remb, capital, interets_bruts, cashback, prelev_sociaux, prelev_forfaitaire,
|
||||
interets_nets, net_recu, statut, distribution_nature
|
||||
FROM remboursements
|
||||
WHERE investissement_pe_id = ?
|
||||
ORDER BY date_remb DESC
|
||||
`).all(row.id);
|
||||
|
||||
const fraisOperations = db.prepare(`
|
||||
SELECT f.id, f.montant, f.mode_reglement, f.categorie, f.date_operation,
|
||||
f.remboursement_id, c.nom AS compte_nom, f.notes
|
||||
FROM frais_operations f
|
||||
LEFT JOIN comptes c ON c.id = f.compte_id
|
||||
WHERE f.investissement_pe_id = ?
|
||||
ORDER BY f.date_operation DESC, f.id DESC
|
||||
`).all(row.id);
|
||||
|
||||
res.json({ ...withTvpi(row), valorisations, remboursements, frais_operations: fraisOperations });
|
||||
} catch (e) { next(e); }
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -21,7 +21,9 @@ const swaggerSpec = swaggerJsdoc({
|
||||
title: 'Crowdlending Tracker API',
|
||||
version: 'v1',
|
||||
description:
|
||||
"API publique en lecture seule du portefeuille de crowdlending. " +
|
||||
"API publique en lecture seule du portefeuille — crowdlending et Private Equity " +
|
||||
"(cf. /investissements pour le crowdlending, /investissements-pe pour le PE ; " +
|
||||
"/remboursements, /depots-retraits et /frais-operations couvrent les deux). " +
|
||||
"Authentification par clé API (header `X-API-Key`), générée depuis Mon compte → Clés API. " +
|
||||
"Chaque clé est scopée à un seul investisseur.",
|
||||
},
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# crowdlending-mcp-server
|
||||
|
||||
Serveur MCP (HTTP, Streamable HTTP transport) pour le portefeuille de
|
||||
crowdlending. Il expose en lecture seule les données d'un investisseur
|
||||
(investissements, remboursements, dépôts/retraits, dashboard) à un client
|
||||
MCP — Claude Desktop, Claude Code, ou tout autre client compatible — en
|
||||
s'appuyant sur l'API publique `/api/v1` du backend.
|
||||
Serveur MCP (HTTP, Streamable HTTP transport) pour le portefeuille —
|
||||
crowdlending et Private Equity. Il expose en lecture seule les données d'un
|
||||
investisseur (investissements crowdlending et PE, remboursements,
|
||||
dépôts/retraits, frais, dashboard) à un client MCP — Claude Desktop, Claude
|
||||
Code, ou tout autre client compatible — en s'appuyant sur l'API publique
|
||||
`/api/v1` du backend.
|
||||
|
||||
Il ne fait aucune écriture : toutes les modifications restent à faire dans
|
||||
l'app web.
|
||||
|
||||
+70
-5
@@ -30,7 +30,7 @@ const READ_ONLY_ANNOTATIONS = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Enregistre les 9 outils de lecture de données sur `server`.
|
||||
* Enregistre les 11 outils de lecture de données sur `server`.
|
||||
*
|
||||
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
||||
* @param {object} ctx
|
||||
@@ -85,7 +85,13 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
|
||||
"depuis le début\" de l'app, toujours un total, jamais filtré par année, " +
|
||||
"reste valable même après un retrait complet des plateformes). Les " +
|
||||
"montants d'investissements sont des soldes actuels (photo à " +
|
||||
"aujourd'hui), pas des cumuls par période. Point d'entrée idéal pour " +
|
||||
"aujourd'hui), pas des cumuls par période. Depuis le 20/09/26, une clé " +
|
||||
"investissements_pe distincte résume le portefeuille Private Equity " +
|
||||
"(nb_deals, total_investi, valorisation_totale, distributions_cumulees, " +
|
||||
"tvpi/dpi/rvpi) — ces chiffres ne sont PAS inclus dans les champs " +
|
||||
"ci-dessus (investissements, cash, gain_net_depuis_debut), qui restent " +
|
||||
"strictement crowdlending ; voir crowdlending_list_investissements_pe " +
|
||||
"pour le détail par deal. Point d'entrée idéal pour " +
|
||||
"une vue d'ensemble avant d'aller chercher le détail."),
|
||||
inputSchema: {
|
||||
annee: z.number().int().optional()
|
||||
@@ -146,6 +152,57 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'crowdlending_list_investissements_pe',
|
||||
{
|
||||
title: withLabel('Liste des investissements Private Equity'),
|
||||
description: withSource(
|
||||
"Liste les investissements Private Equity (deals) du portefeuille — distinct de " +
|
||||
"crowdlending_list_investissements : un deal PE n'a ni échéancier connu à l'avance ni " +
|
||||
"taux d'intérêt fixe, c'est un montant unique investi (montant_investi), valorisé " +
|
||||
"périodiquement (derniere_valorisation, derniere_valorisation_date), avec des " +
|
||||
"distributions perçues au fil du temps (distributions_cumulees, dont un sous-ensemble " +
|
||||
"obligation_cumulee = intérêts sur capital non encore déployé). tvpi (Total Value to " +
|
||||
"Paid-In = (dernière valorisation + distributions cumulées) / montant investi) est null " +
|
||||
"tant qu'aucune valorisation n'a été saisie pour ce deal. Filtrable par statut et par " +
|
||||
"workspace. Ne renvoie pas l'historique des valorisations ni le détail des distributions " +
|
||||
"— utiliser crowdlending_get_investissement_pe pour le détail d'un deal."),
|
||||
inputSchema: {
|
||||
statut: z.enum(['en_attente', 'valide', 'cloture', 'perte_definitive'])
|
||||
.optional()
|
||||
.describe("Filtrer par statut. Omettre pour lister tous les deals."),
|
||||
workspace: z.string().optional()
|
||||
.describe('Slug du workspace à filtrer (utile si plusieurs workspaces Private Equity existent). Absent : tous confondus.'),
|
||||
},
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async ({ statut, workspace }) => {
|
||||
try { return toolResult(await apiGet('/investissements-pe', { statut, workspace })); }
|
||||
catch (e) { return toolError(e); }
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'crowdlending_get_investissement_pe',
|
||||
{
|
||||
title: withLabel("Détail d'un investissement Private Equity"),
|
||||
description: withSource(
|
||||
"Retourne le détail complet d'un deal Private Equity (identifié par son id, obtenu via " +
|
||||
"crowdlending_list_investissements_pe), y compris valorisations (historique complet — " +
|
||||
"la plus récente est celle utilisée pour derniere_valorisation/tvpi dans la liste), " +
|
||||
"remboursements (distributions perçues : retour de capital, plus-value) et " +
|
||||
"frais_operations (tous modes de règlement confondus)."),
|
||||
inputSchema: {
|
||||
id: z.number().int().positive().describe("Identifiant de l'investissement PE"),
|
||||
},
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async ({ id }) => {
|
||||
try { return toolResult(await apiGet(`/investissements-pe/${id}`)); }
|
||||
catch (e) { return toolError(e); }
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
'crowdlending_list_remboursements',
|
||||
{
|
||||
@@ -196,17 +253,25 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
|
||||
"fois. Tous les autres modes (\"source\", \"portefeuille\", " +
|
||||
"\"compte_courant\") représentent chacun une sortie d'argent réelle : " +
|
||||
"c'est cet ensemble (tout sauf \"remboursement\") qui est déduit de " +
|
||||
"gain_net_depuis_debut sur crowdlending_get_dashboard. Filtrable par période."),
|
||||
"gain_net_depuis_debut sur crowdlending_get_dashboard. Depuis le " +
|
||||
"20/09/26, couvre aussi bien les frais crowdlending que les frais " +
|
||||
"Private Equity — `source_type` (\"crowdlending\" ou \"private_equity\") " +
|
||||
"indique de quel côté vient chaque ligne (une ligne crowdlending porte " +
|
||||
"investissement_id/nom_projet, une ligne PE porte investissement_pe_id/" +
|
||||
"nom_deal). Filtrable par période et par workspace (paramètre workspace ; " +
|
||||
"sans ce paramètre, les deux univers sont renvoyés ensemble)."),
|
||||
inputSchema: {
|
||||
date_debut: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
||||
.describe('Date de début au format YYYY-MM-DD (incluse)'),
|
||||
date_fin: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
||||
.describe('Date de fin au format YYYY-MM-DD (incluse)'),
|
||||
workspace: z.string().optional()
|
||||
.describe('Slug du workspace à filtrer (ex. "crowdlending" ou "private-equity"). Absent : les deux univers confondus.'),
|
||||
},
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async ({ date_debut, date_fin }) => {
|
||||
try { return toolResult(await apiGet('/frais-operations', { date_debut, date_fin })); }
|
||||
async ({ date_debut, date_fin, workspace }) => {
|
||||
try { return toolResult(await apiGet('/frais-operations', { date_debut, date_fin, workspace })); }
|
||||
catch (e) { return toolError(e); }
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user