From 4ad6ea79e8ff4e2560bc13cc9cd62edc756f1b1d Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 22 Aug 2026 12:28:48 +0200 Subject: [PATCH] =?UTF-8?q?Am=C3=A9lioration=20de=20l'API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/routes/taxreport.js | 94 ++++++++++++---------- backend/src/routes/v1/dashboard.js | 55 ++++++++++++- backend/src/routes/v1/index.js | 4 + backend/src/routes/v1/objectifs.js | 58 ++++++++++++++ backend/src/routes/v1/taxreport.js | 122 +++++++++++++++++++++++++++++ frontend/src/pages/Aide.jsx | 20 ++++- mcp-server/README.md | 2 + mcp-server/tools.js | 79 ++++++++++++++++++- 8 files changed, 387 insertions(+), 47 deletions(-) create mode 100644 backend/src/routes/v1/objectifs.js create mode 100644 backend/src/routes/v1/taxreport.js diff --git a/backend/src/routes/taxreport.js b/backend/src/routes/taxreport.js index 008a01b..3c1d7dc 100644 --- a/backend/src/routes/taxreport.js +++ b/backend/src/routes/taxreport.js @@ -201,15 +201,7 @@ router.get('/export', (req, res) => { res.send('\uFEFF' + csv); }); -/* ── 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 invArg = scopeAll ? req.user.id : req.investisseur.id; - +function buildCerfa2561(annee, invCond, invCondBonus2561, invArg) { const rows = db.prepare(` SELECT 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 // 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. - const invCondBonus2561 = scopeAll - ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' - : 'r.bonus_investisseur_id = ?'; const rowsInteretsPlateforme = db.prepare(` SELECT p.id AS plateforme_id, @@ -399,7 +388,23 @@ router.get('/cerfa2561', (req, res) => { 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 ── */ @@ -506,15 +511,7 @@ router.get('/years', (req, res) => { -/* ── 2778-SD — matrice mensuelle par plateforme étrangère ── */ -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; - +function build2778(annee, invCond, invCondBonus2778, invArg) { // Plateformes étrangères × investisseur avec au moins un remboursement sur l'année 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 @@ -532,9 +529,6 @@ router.get('/2778', (req, res) => { // 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. - const invCondBonus2778 = scopeAll - ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' - : 'r.bonus_investisseur_id = ?'; 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 FROM remboursements r @@ -620,26 +614,28 @@ router.get('/2778', (req, res) => { 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), - 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) => { + + +/* ── 2778-SD — matrice mensuelle par plateforme étrangère ── */ +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 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; + res.json(build2778(annee, invCond, invCondBonus2778, invArg)); +}); +function build2047(annee, invCond, invCondBonus2047, invArg) { const rows = db.prepare(` SELECT 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 `).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(` SELECT 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. 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; diff --git a/backend/src/routes/v1/dashboard.js b/backend/src/routes/v1/dashboard.js index 349d2c5..aef5b31 100644 --- a/backend/src/routes/v1/dashboard.js +++ b/backend/src/routes/v1/dashboard.js @@ -3,6 +3,8 @@ import db from '../../db/index.js'; const router = Router(); +const round2 = v => Math.round((v ?? 0) * 100) / 100; + /** * @openapi * /dashboard: @@ -13,7 +15,12 @@ const router = Router(); * aujourd'hui), pas des cumuls par période — un investissement remboursé * partiellement reste compté pour son capital restant dû tant qu'il * 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] * security: [{ ApiKeyAuth: [] }] * parameters: @@ -84,7 +91,51 @@ router.get('/', (req, res) => { FROM depots_retraits WHERE ${invCond('investisseur_id')} `).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; diff --git a/backend/src/routes/v1/index.js b/backend/src/routes/v1/index.js index 5e219c8..2466c85 100644 --- a/backend/src/routes/v1/index.js +++ b/backend/src/routes/v1/index.js @@ -4,6 +4,8 @@ import investissementsRouter from './investissements.js'; import remboursementsRouter from './remboursements.js'; import depotsRetraitsRouter from './depotsRetraits.js'; import dashboardRouter from './dashboard.js'; +import objectifsRouter from './objectifs.js'; +import taxreportRouter from './taxreport.js'; const router = Router(); @@ -12,5 +14,7 @@ router.use('/investissements', investissementsRouter); router.use('/remboursements', remboursementsRouter); router.use('/depots-retraits', depotsRetraitsRouter); router.use('/dashboard', dashboardRouter); +router.use('/objectifs', objectifsRouter); +router.use('/taxreport', taxreportRouter); export default router; diff --git a/backend/src/routes/v1/objectifs.js b/backend/src/routes/v1/objectifs.js new file mode 100644 index 0000000..4ccb097 --- /dev/null +++ b/backend/src/routes/v1/objectifs.js @@ -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; diff --git a/backend/src/routes/v1/taxreport.js b/backend/src/routes/v1/taxreport.js new file mode 100644 index 0000000..5375b6e --- /dev/null +++ b/backend/src/routes/v1/taxreport.js @@ -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; diff --git a/frontend/src/pages/Aide.jsx b/frontend/src/pages/Aide.jsx index e3618d7..c024f5d 100644 --- a/frontend/src/pages/Aide.jsx +++ b/frontend/src/pages/Aide.jsx @@ -594,6 +594,22 @@ export default function Aide() { Historique des mouvements de cash (dépôts et retraits), du plus récent au plus ancien. + + + crowdlending_list_objectifs + + Objectifs de versement et de capital investi (Paramétrage → Fixation des objectifs), à comparer au réalisé. Filtrable par type et par année. + + + + crowdlending_get_taxreport + + + 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. + + crowdlending_fetch_url @@ -617,10 +633,12 @@ export default function Aide() {
  • « Quels ont été mes derniers dépôts et retraits ? »
  • « Regarde cette annonce de projet et propose-moi les infos pour créer l'investissement : [URL] » (développement local, avec crowdlending_fetch_url activé)
  • +
  • « Ai-je atteint mon objectif de versement pour {new Date().getFullYear()} ? »
  • +
  • « Prépare-moi une synthèse fiscale pour l'année {new Date().getFullYear() - 1}. »
  • 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.

    diff --git a/mcp-server/README.md b/mcp-server/README.md index e7248b3..462935d 100644 --- a/mcp-server/README.md +++ b/mcp-server/README.md @@ -213,6 +213,8 @@ Tous en lecture seule (`readOnlyHint: true`) : | `crowdlending_get_investissement` | Détail d'un investissement + ses remboursements | | `crowdlending_list_remboursements` | Historique des remboursements, filtrable par période | | `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 | ## Lire une annonce de projet (`crowdlending_fetch_url`) diff --git a/mcp-server/tools.js b/mcp-server/tools.js index ebc0149..b2fbda3 100644 --- a/mcp-server/tools.js +++ b/mcp-server/tools.js @@ -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 {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 " + "KPI \"Capital investi\" de l'app), capital en risque (sous-ensemble en " + "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 " + - "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."), + "capital reçu, total dépôts/retraits, et gain_net_depuis_debut (intérêts " + + "nets + cashback/bonus de parrainage ou de plateforme + corrections de " + + "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: { 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."), @@ -166,6 +171,72 @@ export function registerDataTools(server, { apiGet, withLabel = (t) => t, withSo 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.