Amélioration de l'API
This commit is contained in:
1 parent
5dc6f21e61
commit
4ad6ea79e8
8 files changed
+387
-47
No files matched your search
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -594,6 +594,22 @@ export default function Aide() {
|
||||
</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 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>
|
||||
<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>
|
||||
@@ -617,10 +633,12 @@ export default function Aide() {
|
||||
<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] »
|
||||
(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>
|
||||
<p style={{ marginBottom: 0 }}>
|
||||
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>
|
||||
</FaqItem>
|
||||
|
||||
|
||||
@@ -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`)
|
||||
|
||||
+75
-4
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user