Amélioration de l'API

This commit is contained in:
ocroguennec committed 2026-08-22 12:28:48 +02:00
1 parent 5dc6f21e61
commit 4ad6ea79e8
8 files changed
+387 -47

No files matched your search

+54 -40
View File
@@ -201,15 +201,7 @@ router.get('/export', (req, res) => {
res.send('\uFEFF' + csv); res.send('\uFEFF' + csv);
}); });
/* ── CERFA 2561 — synthèse par plateforme × investisseur ── */ function buildCerfa2561(annee, invCond, invCondBonus2561, invArg) {
router.get('/cerfa2561', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all';
const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?';
const invArg = scopeAll ? req.user.id : req.investisseur.id;
const rows = db.prepare(` const rows = db.prepare(`
SELECT SELECT
p.id AS plateforme_id, p.id AS plateforme_id,
@@ -239,9 +231,6 @@ router.get('/cerfa2561', (req, res) => {
// bonus_investisseur_id) : fusionnés sur la même ligne plateforme × investisseur que // bonus_investisseur_id) : fusionnés sur la même ligne plateforme × investisseur que
// les intérêts d'investissement classiques, car la fiscalité (domiciliation/fiscalite/ // les intérêts d'investissement classiques, car la fiscalité (domiciliation/fiscalite/
// type_produit_fiscal) dépend de la plateforme, pas de l'investissement précis. // type_produit_fiscal) dépend de la plateforme, pas de l'investissement précis.
const invCondBonus2561 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const rowsInteretsPlateforme = db.prepare(` const rowsInteretsPlateforme = db.prepare(`
SELECT SELECT
p.id AS plateforme_id, p.id AS plateforme_id,
@@ -399,7 +388,23 @@ router.get('/cerfa2561', (req, res) => {
mois: moisMap[`${l.plateforme_id}_${l.investisseur_id}`] ?? {}, mois: moisMap[`${l.plateforme_id}_${l.investisseur_id}`] ?? {},
})); }));
res.json({ annee, lignes: lignesWithMois }); return { annee, lignes: lignesWithMois };
}
/* ── CERFA 2561 — synthèse par plateforme × investisseur ── */
router.get('/cerfa2561', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all';
const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?';
const invCondBonus2561 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const invArg = scopeAll ? req.user.id : req.investisseur.id;
res.json(buildCerfa2561(annee, invCond, invCondBonus2561, invArg));
}); });
/* ── CERFA 2561 — détail des remboursements par plateforme × investisseur ── */ /* ── CERFA 2561 — détail des remboursements par plateforme × investisseur ── */
@@ -506,15 +511,7 @@ router.get('/years', (req, res) => {
/* ── 2778-SD — matrice mensuelle par plateforme étrangère ── */ function build2778(annee, invCond, invCondBonus2778, invArg) {
router.get('/2778', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all';
const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?';
const invArg = scopeAll ? req.user.id : req.investisseur.id;
// Plateformes étrangères × investisseur avec au moins un remboursement sur l'année // Plateformes étrangères × investisseur avec au moins un remboursement sur l'année
const platRows = db.prepare(` const platRows = db.prepare(`
SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
@@ -532,9 +529,6 @@ router.get('/2778', (req, res) => {
// Intérêts plateforme (pas d'investissement) sur des plateformes étrangères — même // Intérêts plateforme (pas d'investissement) sur des plateformes étrangères — même
// logique de fusion que pour le 2561 : rattachés à la ligne plateforme × investisseur. // logique de fusion que pour le 2561 : rattachés à la ligne plateforme × investisseur.
const invCondBonus2778 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const platRowsInteretsPlateforme = db.prepare(` const platRowsInteretsPlateforme = db.prepare(`
SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
FROM remboursements r FROM remboursements r
@@ -620,26 +614,28 @@ router.get('/2778', (req, res) => {
mois: moisMap[`${p.id}_${p.investisseur_id}`] ?? {}, mois: moisMap[`${p.id}_${p.investisseur_id}`] ?? {},
})); }));
res.json({ annee, plateformes }); return { annee, plateformes };
}); }
/* ── 2047 — Cadre 2, rubrique 230 (Intérêts) — vision annuelle par pays/plateforme ──
Détermine, pour chaque plateforme étrangère × investisseur, si les intérêts perçus
ouvrent droit à un crédit d'impôt conventionnel (cases 231-238) ou non (case 250), /* ── 2778-SD — matrice mensuelle par plateforme étrangère ── */
en s'appuyant sur le référentiel pays "taux_credit_impot" (paramétrage admin). router.get('/2778', (req, res) => {
Éligibilité = convention active + taux d'intérêt renseigné + non "exclusif résidence"
(le pictogramme c/ de la notice signifie imposable exclusivement en France : aucun
crédit d'impôt possible, même si la plateforme a réellement prélevé une taxe locale). ── */
router.get('/2047', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1); const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all'; const scopeAll = req.query.scope === 'all';
const invCond = scopeAll const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' ? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?'; : 'i.investisseur_id = ?';
const invCondBonus2778 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const invArg = scopeAll ? req.user.id : req.investisseur.id; const invArg = scopeAll ? req.user.id : req.investisseur.id;
res.json(build2778(annee, invCond, invCondBonus2778, invArg));
});
function build2047(annee, invCond, invCondBonus2047, invArg) {
const rows = db.prepare(` const rows = db.prepare(`
SELECT SELECT
p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation, p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation,
@@ -663,10 +659,6 @@ router.get('/2047', (req, res) => {
GROUP BY p.id, inv.id GROUP BY p.id, inv.id
`).all(invArg, annee); `).all(invArg, annee);
// Intérêts plateforme (bonus, sans investissement rattaché) — même logique de fusion que /2778
const invCondBonus2047 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const rowsInteretsPlateforme = db.prepare(` const rowsInteretsPlateforme = db.prepare(`
SELECT SELECT
p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation, p.id AS plateforme_id, p.nom AS plateforme_nom, p.domiciliation,
@@ -789,7 +781,29 @@ router.get('/2047', (req, res) => {
// (info complémentaire, non reportée sur une case du formulaire) reste arrondi au centime. // (info complémentaire, non reportée sur une case du formulaire) reste arrondi au centime.
totals.montant_avant_local = round2(totals.montant_avant_local); totals.montant_avant_local = round2(totals.montant_avant_local);
res.json({ annee, lignes, totals }); return { annee, lignes, totals };
}
/* ── 2047 — Cadre 2, rubrique 230 (Intérêts) — vision annuelle par pays/plateforme ──
Détermine, pour chaque plateforme étrangère × investisseur, si les intérêts perçus
ouvrent droit à un crédit d'impôt conventionnel (cases 231-238) ou non (case 250),
en s'appuyant sur le référentiel pays "taux_credit_impot" (paramétrage admin).
Éligibilité = convention active + taux d'intérêt renseigné + non "exclusif résidence"
(le pictogramme c/ de la notice signifie imposable exclusivement en France : aucun
crédit d'impôt possible, même si la plateforme a réellement prélevé une taxe locale). ── */
router.get('/2047', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all';
const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?';
const invCondBonus2047 = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
const invArg = scopeAll ? req.user.id : req.investisseur.id;
res.json(build2047(annee, invCond, invCondBonus2047, invArg));
}); });
export { buildCerfa2561, build2778, build2047 };
export default router; export default router;
+53 -2
View File
@@ -3,6 +3,8 @@ import db from '../../db/index.js';
const router = Router(); const router = Router();
const round2 = v => Math.round((v ?? 0) * 100) / 100;
/** /**
* @openapi * @openapi
* /dashboard: * /dashboard:
@@ -13,7 +15,12 @@ const router = Router();
* aujourd'hui), pas des cumuls par période — un investissement remboursé * aujourd'hui), pas des cumuls par période — un investissement remboursé
* partiellement reste compté pour son capital restant dû tant qu'il * partiellement reste compté pour son capital restant dû tant qu'il
* n'est pas soldé. Seuls les champs `interets.*` peuvent être filtrés * n'est pas soldé. Seuls les champs `interets.*` peuvent être filtrés
* par année via `?annee=`. * par année via `?annee=`. `gain_net_depuis_debut` est TOUJOURS un cumul
* total (indépendant de `?annee=`) : intérêts nets + cashback/bonus de
* parrainage ou de plateforme + corrections de solde, 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.
* tags: [Dashboard] * tags: [Dashboard]
* security: [{ ApiKeyAuth: [] }] * security: [{ ApiKeyAuth: [] }]
* parameters: * parameters:
@@ -84,7 +91,51 @@ router.get('/', (req, res) => {
FROM depots_retraits WHERE ${invCond('investisseur_id')} FROM depots_retraits WHERE ${invCond('investisseur_id')}
`).get(invParam); `).get(invParam);
res.json({ investissements, interets: { ...interets, annee: annee || null }, cash }); // ── Gain net depuis le début — même formule que le KPI du Dashboard interne :
// Σ(intérêts nets, tout l'historique) + Σ(cashback + bonus parrainage/plateforme,
// tout l'historique) + Σ(corrections de solde, tout l'historique). Volontairement
// TOUJOURS un cumul total, jamais filtré par `annee` (cf. description ci-dessus) —
// c'est ce qui le rend robuste à un retrait complet des plateformes, contrairement
// à une approche par solde. La clause OR ci-dessous inclut aussi bien les
// remboursements liés à un investissement que les "intérêts plateforme"/bonus de
// parrainage rattachés via bonus_investisseur_id (sans investissement), en miroir
// exact de GET /api/dashboard/interets-annuels.
const gainNetCond = req.investisseurScopeAll
? '(r.investissement_id IS NOT NULL AND own.user_id = ?) OR (r.bonus_investisseur_id IS NOT NULL AND bonus_own.user_id = ?)'
: '(r.investissement_id IS NOT NULL AND inv.investisseur_id = ?) OR (r.bonus_investisseur_id IS NOT NULL AND r.bonus_investisseur_id = ?)';
const gainNetRow = db.prepare(`
SELECT
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets_total,
COALESCE(SUM(r.cashback), 0) AS cashback_total
FROM remboursements r
LEFT JOIN investissements inv ON inv.id = r.investissement_id
LEFT JOIN investisseurs own ON own.id = inv.investisseur_id
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
WHERE ${gainNetCond}
`).get(invParam, invParam);
const correctionsRow = db.prepare(`
SELECT COALESCE(SUM(montant), 0) AS total
FROM corrections_solde
WHERE ${invCond('investisseur_id')}
`).get(invParam);
const gainNetTotal = round2(
gainNetRow.interets_nets_total + gainNetRow.cashback_total + correctionsRow.total
);
res.json({
investissements,
interets: { ...interets, annee: annee || null },
cash,
gain_net_depuis_debut: {
total: gainNetTotal,
interets_nets: round2(gainNetRow.interets_nets_total),
cashback: round2(gainNetRow.cashback_total),
corrections: round2(correctionsRow.total),
},
});
}); });
export default router; export default router;
+4
View File
@@ -4,6 +4,8 @@ import investissementsRouter from './investissements.js';
import remboursementsRouter from './remboursements.js'; import remboursementsRouter from './remboursements.js';
import depotsRetraitsRouter from './depotsRetraits.js'; import depotsRetraitsRouter from './depotsRetraits.js';
import dashboardRouter from './dashboard.js'; import dashboardRouter from './dashboard.js';
import objectifsRouter from './objectifs.js';
import taxreportRouter from './taxreport.js';
const router = Router(); const router = Router();
@@ -12,5 +14,7 @@ router.use('/investissements', investissementsRouter);
router.use('/remboursements', remboursementsRouter); router.use('/remboursements', remboursementsRouter);
router.use('/depots-retraits', depotsRetraitsRouter); router.use('/depots-retraits', depotsRetraitsRouter);
router.use('/dashboard', dashboardRouter); router.use('/dashboard', dashboardRouter);
router.use('/objectifs', objectifsRouter);
router.use('/taxreport', taxreportRouter);
export default router; export default router;
+58
View File
@@ -0,0 +1,58 @@
import { Router } from 'express';
import db from '../../db/index.js';
const router = Router();
/**
* @openapi
* /objectifs:
* get:
* summary: Objectifs de versement et de capital investi
* description: >
* Objectifs fixés dans Paramétrage → Fixation des objectifs, à comparer au réalisé
* (voir crowdlending_get_dashboard / crowdlending_list_depots_retraits pour le
* réalisé). Trois types : `versement_global` (objectif global sur l'investisseur
* principal), `versement_annuel` (enveloppe par détenteur), `capital_investi`
* (objectif de capital investi par plateforme, `plateforme_id` renseigné). La liste
* est vide si la fonctionnalité est désactivée par l'utilisateur, ou si aucun
* objectif n'a encore été saisi.
* tags: [Objectifs]
* security: [{ ApiKeyAuth: [] }]
* parameters:
* - in: query
* name: type
* schema: { type: string, enum: [versement_global, versement_annuel, capital_investi] }
* description: Filtrer par type d'objectif.
* - in: query
* name: annee
* schema: { type: integer }
* description: Filtrer par année.
* responses:
* 200: { description: Liste des objectifs }
*/
router.get('/', (req, res) => {
const { type, annee } = req.query;
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du foyer ;
// clé mono-investisseur → uniquement les objectifs de cet investisseur (contrairement à
// la route interne /api/objectifs, qui renvoie tout le foyer car elle est scopée par
// session JWT — une clé API mono-investisseur ne doit voir que ses propres objectifs).
const conds = [req.investisseurScopeAll
? 'o.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'o.investisseur_id = ?'];
const args = [req.investisseurScopeAll ? req.userId : req.investisseurId];
if (type) { conds.push('o.type = ?'); args.push(type); }
if (annee) { conds.push('o.annee = ?'); args.push(Number(annee)); }
const rows = db.prepare(`
SELECT o.*, i.nom AS investisseur_nom, p.nom AS plateforme_nom
FROM objectifs o
JOIN investisseurs i ON i.id = o.investisseur_id
LEFT JOIN plateformes p ON p.id = o.plateforme_id
WHERE ${conds.join(' AND ')}
ORDER BY o.annee DESC, i.nom
`).all(...args);
res.json(rows);
});
export default router;
+122
View File
@@ -0,0 +1,122 @@
import { Router } from 'express';
import db from '../../db/index.js';
import { buildCerfa2561, build2778, build2047 } from '../taxreport.js';
const router = Router();
const sumBy = (arr, key) => arr.reduce((s, x) => s + (x[key] ?? 0), 0);
/**
* @openapi
* /taxreport:
* get:
* summary: Synthèse fiscale annuelle (CERFA 2042 / 2561 / 2047 / 2778-SD)
* description: >
* Aide à la préparation de la déclaration de revenus, calculée avec exactement les
* mêmes règles que la page Fiscalité de l'application (mêmes fonctions backend que
* les routes internes /taxreport/cerfa2561, /taxreport/2778 et /taxreport/2047).
* Ce n'est PAS une source officielle : vérifiez chaque montant avant de le reporter
* sur votre déclaration, vous restez seul responsable de son exactitude.
*
* `synthese_2042` reprend les cases 2TT / 2TR / 2BH / 2CK / 2TY telles qu'affichées
* en haut de la page Fiscalité. `cerfa2561` détaille le récapitulatif IFU des
* plateformes françaises soumises au PFU. `cerfa2047` (présent seulement si
* `has_etranger`) détaille, par plateforme étrangère, l'éligibilité au crédit
* d'impôt conventionnel (cases 231 à 238 vs 250) et le report vers 8VL/8PL.
* `cerfa2778_mensuel` (présent seulement si `has_etranger`) donne le détail mensuel
* des intérêts bruts par plateforme étrangère, base de calcul du prélèvement
* forfaitaire obligatoire (PFO) — le PFO n'est légalement dû que si `pfo_assujetti`
* est vrai (seuil de revenu fiscal de référence, cf. Paramétrage → Ma fiscalité).
*
* Limite connue : `case_2BH` / `case_2CK` de `synthese_2042` supposent qu'aucune
* plateforme étrangère n'a été exclue manuellement du calcul PFO dans l'application
* (réglage propre au navigateur, non accessible depuis l'API) — en cas d'exclusion,
* la valeur affichée dans l'app peut différer légèrement de celle-ci.
* tags: [Fiscalité]
* security: [{ ApiKeyAuth: [] }]
* parameters:
* - in: query
* name: annee
* schema: { type: integer }
* description: Année fiscale (ex. 2026). Par défaut, l'année précédente.
* responses:
* 200: { description: Synthèse fiscale annuelle }
*/
router.get('/', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.investisseurScopeAll;
const invParam = scopeAll ? req.userId : req.investisseurId;
// Clé "Famille et entreprises" (scope_all) → tous les investisseurs du foyer ;
// clé mono-investisseur → filtre sur req.investisseurId. Mêmes fragments SQL que ceux
// construits par les routes internes /taxreport/* (cf. backend/src/routes/taxreport.js),
// passés en paramètre aux fonctions partagées buildCerfa2561/build2778/build2047 pour
// garantir des résultats identiques à ceux affichés dans l'application.
const invCond = scopeAll
? 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'i.investisseur_id = ?';
const invCondBonus = scopeAll
? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'
: 'r.bonus_investisseur_id = ?';
// pfo_assujetti est une préférence GLOBALE de l'utilisateur (table user_preferences,
// clé user_id) — même mécanisme que UiContext.jsx côté frontend, pas une donnée liée à
// un investisseur en particulier.
const prefRow = db.prepare(
`SELECT value FROM user_preferences WHERE user_id = ? AND key = 'pfo_assujetti'`
).get(req.userId);
const pfoAssujetti = prefRow ? prefRow.value === 'true' : false;
// Au moins une plateforme domiciliée hors de France, tous détenteurs du foyer confondus
// et indépendamment de l'année — la table `plateformes` est scopée par `user_id` (pas
// `investisseur_id`, qui n'est que le détenteur assigné) dans toute l'application (cf.
// GET /api/plateformes), et hasEtranger côté frontend (TaxReport.jsx) est calculé sur
// cette même liste non filtrée par investisseur actif — un comportement volontairement
// répliqué ici, même avec une clé API mono-investisseur.
const hasEtranger = !!db.prepare(
`SELECT 1 FROM plateformes WHERE user_id = ? AND domiciliation != 'FR' LIMIT 1`
).get(req.userId);
const cerfa2561 = buildCerfa2561(annee, invCond, invCondBonus, invParam);
let cerfa2047 = null;
let cerfa2778_mensuel = null;
if (hasEtranger) {
cerfa2047 = build2047(annee, invCond, invCondBonus, invParam);
cerfa2778_mensuel = build2778(annee, invCond, invCondBonus, invParam);
}
// ── Synthèse 2042 — mêmes cases et mêmes formules que le pavé "Cases fiscales 2042 —
// synthèse" en haut de la page Fiscalité (TaxReport.jsx, fonction load()) :
// - 2TT / 2TY : cumul direct des lignes 2561 françaises.
// - 2TR : cumul des lignes 2561 françaises + brut étranger (toujours reporté, que le
// PFO s'applique ou non — seule la déclaration annuelle 2047 est en jeu ici).
// - 2BH / 2CK : la part étrangère n'est ajoutée que si pfo_assujetti est vrai, sinon
// ces cases ne sont pas censées être alimentées (cf. mémoire projet — la case 2CK
// ne s'alimente que si la déclaration 2778-SD est active).
const frLignes = cerfa2561.lignes.filter(l => l.domiciliation === 'FR');
const platEtr = cerfa2778_mensuel?.plateformes ?? [];
const etrBA = p => Object.values(p.mois ?? {}).reduce((s, v) => s + v, 0);
const totalBA = platEtr.reduce((s, p) => s + etrBA(p), 0);
const PFO_RATE = 0.128; // même valeur par défaut que celle utilisée par TaxReport.jsx pour cette synthèse
const synthese_2042 = {
case_2TT: sumBy(frLignes, 'case_2TT'),
case_2TR: sumBy(frLignes, 'case_2TR') + Math.round(totalBA),
case_2BH: sumBy(frLignes, 'case_2BH') + (pfoAssujetti ? Math.round(totalBA) : 0),
case_2CK: sumBy(frLignes, 'case_2CK') + (pfoAssujetti ? Math.round(totalBA * PFO_RATE) : 0),
case_2TY: sumBy(frLignes, 'case_2TY'),
};
res.json({
annee,
has_etranger: hasEtranger,
pfo_assujetti: pfoAssujetti,
synthese_2042,
cerfa2561,
cerfa2047,
cerfa2778_mensuel,
});
});
export default router;
+19 -1
View File
@@ -594,6 +594,22 @@ export default function Aide() {
</td> </td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des mouvements de cash (dépôts et retraits), du plus récent au plus ancien.</td> <td style={{ padding: '8px 0', verticalAlign: 'top' }}>Historique des mouvements de cash (dépôts et retraits), du plus récent au plus ancien.</td>
</tr> </tr>
<tr style={{ borderBottom: '1px solid var(--border)' }}>
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_list_objectifs</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>Objectifs de versement et de capital investi (Paramétrage → Fixation des objectifs), à comparer au réalisé. Filtrable par type et par année.</td>
</tr>
<tr style={{ borderBottom: '1px solid var(--border)' }}>
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_get_taxreport</code>
</td>
<td style={{ padding: '8px 0', verticalAlign: 'top' }}>
Synthèse fiscale annuelle (CERFA 2042/2561/2047/2778-SD) — aide à la préparation de la déclaration,
calculée avec les mêmes règles que la page Fiscalité. Ce n'est pas une source officielle : à vérifier
avant tout report sur votre déclaration.
</td>
</tr>
<tr> <tr>
<td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}> <td style={{ padding: '8px 10px 8px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }}>
<code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code> <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code>
@@ -617,10 +633,12 @@ export default function Aide() {
<li>« Quels ont été mes derniers dépôts et retraits ? »</li> <li>« Quels ont été mes derniers dépôts et retraits ? »</li>
<li>« Regarde cette annonce de projet et propose-moi les infos pour créer l'investissement : [URL] » <li>« Regarde cette annonce de projet et propose-moi les infos pour créer l'investissement : [URL] »
(développement local, avec <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code> activé)</li> (développement local, avec <code style={{ background: 'var(--surface-2)', padding: '1px 5px', borderRadius: 4 }}>crowdlending_fetch_url</code> activé)</li>
<li>« Ai-je atteint mon objectif de versement pour {new Date().getFullYear()} ? »</li>
<li>« Prépare-moi une synthèse fiscale pour l'année {new Date().getFullYear() - 1}. »</li>
</ul> </ul>
<p style={{ marginBottom: 0 }}> <p style={{ marginBottom: 0 }}>
Ces exemples fonctionnent aussi bien en dev qu'en prod dès lors que le serveur correspondant est connecté Ces exemples fonctionnent aussi bien en dev qu'en prod dès lors que le serveur correspondant est connecté
(voir les FAQ de configuration ci-dessus) — les six premiers outils sont identiques dans les deux environnements. (voir les FAQ de configuration ci-dessus) — les huit premiers outils sont identiques dans les deux environnements.
</p> </p>
</FaqItem> </FaqItem>
+2
View File
@@ -213,6 +213,8 @@ Tous en lecture seule (`readOnlyHint: true`) :
| `crowdlending_get_investissement` | Détail d'un investissement + ses remboursements | | `crowdlending_get_investissement` | Détail d'un investissement + ses remboursements |
| `crowdlending_list_remboursements` | Historique des remboursements, filtrable par période | | `crowdlending_list_remboursements` | Historique des remboursements, filtrable par période |
| `crowdlending_list_depots_retraits` | Historique des mouvements de cash | | `crowdlending_list_depots_retraits` | Historique des mouvements de cash |
| `crowdlending_list_objectifs` | Objectifs de versement et de capital investi, à comparer au réalisé |
| `crowdlending_get_taxreport` | Synthèse fiscale annuelle (CERFA 2042/2561/2047/2778-SD) — aide à la déclaration, pas une source officielle |
| `crowdlending_fetch_url` | *(actif seulement si `MCP_ENABLE_FETCH_URL=true`)* Lit une page web (annonce de projet) et en extrait le texte propre — voir ci-dessous | | `crowdlending_fetch_url` | *(actif seulement si `MCP_ENABLE_FETCH_URL=true`)* Lit une page web (annonce de projet) et en extrait le texte propre — voir ci-dessous |
## Lire une annonce de projet (`crowdlending_fetch_url`) ## Lire une annonce de projet (`crowdlending_fetch_url`)
+75 -4
View File
@@ -30,7 +30,7 @@ const READ_ONLY_ANNOTATIONS = {
}; };
/** /**
* Enregistre les 6 outils de lecture de données sur `server`. * Enregistre les 9 outils de lecture de données sur `server`.
* *
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
* @param {object} ctx * @param {object} ctx
@@ -74,9 +74,14 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
"cours et en défaut, net des remboursements déjà perçus — équivalent au " + "cours et en défaut, net des remboursements déjà perçus — équivalent au " +
"KPI \"Capital investi\" de l'app), capital en risque (sous-ensemble en " + "KPI \"Capital investi\" de l'app), capital en risque (sous-ensemble en " +
"retard/procédure), montant remboursé, intérêts bruts/nets perçus, " + "retard/procédure), montant remboursé, intérêts bruts/nets perçus, " +
"capital reçu, total dépôts/retraits. Les montants d'investissements sont " + "capital reçu, total dépôts/retraits, et gain_net_depuis_debut (intérêts " +
"des soldes actuels (photo à aujourd'hui), pas des cumuls par période. " + "nets + cashback/bonus de parrainage ou de plateforme + corrections de " +
"Point d'entrée idéal pour une vue d'ensemble avant d'aller chercher le détail."), "solde, cumulés sur tout l'historique — équivalent au KPI \"Gain net " +
"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 " +
"une vue d'ensemble avant d'aller chercher le détail."),
inputSchema: { inputSchema: {
annee: z.number().int().optional() annee: z.number().int().optional()
.describe("Filtre les intérêts/capital reçu sur une année (ex. 2026). Omettre pour le cumul total. N'affecte pas le capital investi/en risque, qui sont toujours des soldes actuels."), .describe("Filtre les intérêts/capital reçu sur une année (ex. 2026). Omettre pour le cumul total. N'affecte pas le capital investi/en risque, qui sont toujours des soldes actuels."),
@@ -166,6 +171,72 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo
catch (e) { return toolError(e); } catch (e) { return toolError(e); }
}, },
); );
server.registerTool(
'crowdlending_list_objectifs',
{
title: withLabel('Objectifs de versement et de capital investi'),
description: withSource(
"Liste les objectifs fixés dans Paramétrage → Fixation des objectifs, à " +
"comparer au réalisé (voir crowdlending_get_dashboard ou " +
"crowdlending_list_depots_retraits). Trois types : versement_global " +
"(objectif global sur l'investisseur principal), versement_annuel " +
"(enveloppe par détenteur), capital_investi (objectif de capital investi " +
"par plateforme, avec plateforme_id renseigné). Liste vide si la " +
"fonctionnalité est désactivée par l'utilisateur, ou si aucun objectif " +
"n'a encore été saisi."),
inputSchema: {
type: z.enum(['versement_global', 'versement_annuel', 'capital_investi'])
.optional()
.describe("Filtrer par type d'objectif. Omettre pour lister tous les objectifs."),
annee: z.number().int().optional()
.describe("Filtrer par année. Omettre pour lister toutes les années."),
},
annotations: READ_ONLY_ANNOTATIONS,
},
async ({ type, annee }) => {
try { return toolResult(await apiGet('/objectifs', { type, annee })); }
catch (e) { return toolError(e); }
},
);
server.registerTool(
'crowdlending_get_taxreport',
{
title: withLabel('Synthèse fiscale annuelle (CERFA)'),
description: withSource(
"Retourne une synthèse fiscale annuelle pour aider à préparer la " +
"déclaration de revenus — CE N'EST PAS UNE SOURCE OFFICIELLE : les " +
"montants doivent être vérifiés avant d'être reportés, l'utilisateur " +
"reste seul responsable de l'exactitude de sa déclaration. Reprend les " +
"mêmes calculs que la page Fiscalité de l'application. `synthese_2042` " +
"donne les cases 2TT/2TR/2BH/2CK/2TY à reporter sur la déclaration " +
"principale (CERFA 2042). `cerfa2561` détaille le récapitulatif IFU des " +
"plateformes françaises soumises au prélèvement forfaitaire unique " +
"(PFU). `cerfa2047` (présent seulement si has_etranger est vrai) détaille " +
"par plateforme étrangère l'éligibilité au crédit d'impôt conventionnel " +
"(cases 231 à 238, ou 250 si non éligible). `cerfa2778_mensuel` (présent " +
"seulement si has_etranger) donne le détail mensuel des intérêts bruts " +
"par plateforme étrangère, base du calcul du prélèvement forfaitaire " +
"obligatoire (PFO) — un acompte mensuel légalement dû seulement si " +
"pfo_assujetti est vrai (seuil de revenu fiscal de référence : 25 000 € " +
"pour une personne seule, 50 000 € pour un couple marié ou pacsé), mais " +
"la déclaration annuelle CERFA 2047 reste obligatoire dans tous les cas " +
"dès qu'il y a des revenus de plateformes étrangères. Limite connue : " +
"case_2BH/case_2CK de synthese_2042 supposent qu'aucune plateforme " +
"étrangère n'a été exclue manuellement du calcul PFO dans l'application " +
"(réglage propre au navigateur, non accessible depuis l'API)."),
inputSchema: {
annee: z.number().int().optional()
.describe("Année fiscale (ex. 2026). Omettre pour l'année précédente par défaut."),
},
annotations: READ_ONLY_ANNOTATIONS,
},
async ({ annee }) => {
try { return toolResult(await apiGet('/taxreport', { annee })); }
catch (e) { return toolError(e); }
},
);
} }
/** Formate le résultat d'un outil : texte JSON lisible + structuredContent. /** Formate le résultat d'un outil : texte JSON lisible + structuredContent.