diff --git a/backend/src/routes/taxreport.js b/backend/src/routes/taxreport.js index 424ab1f..6711dec 100644 --- a/backend/src/routes/taxreport.js +++ b/backend/src/routes/taxreport.js @@ -6,6 +6,10 @@ const router = Router(); router.use(requireInvestisseur); const round2 = v => Math.round((v ?? 0) * 100) / 100; +// Échappement CSV (séparateur ';') — remplace les caractères qui casseraient les colonnes. +// Bug préexistant : cette fonction était utilisée plus bas dans /export sans être définie +// (ReferenceError à l'exécution), corrigé au passage. +const esc = v => String(v ?? '').replace(/;/g, ',').replace(/[\r\n]+/g, ' '); router.get('/', (req, res) => { const annee = req.query.annee || String(new Date().getFullYear() - 1); @@ -29,6 +33,32 @@ router.get('/', (req, res) => { AND r.statut IN ('paye','partiel') `).get(invArg, annee); + // Intérêts plateforme (type=interets_plateforme, pas d'investissement — rattachés via + // bonus_plateforme_id/bonus_investisseur_id) : mêmes règles fiscales que la plateforme + // associée, donc à additionner aux totaux comme des intérêts d'investissement classiques. + const invCondBonus = scopeAll + ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'r.bonus_investisseur_id = ?'; + const recapInteretsPlateforme = db.prepare(` + SELECT + COALESCE(SUM(r.interets_bruts),0) AS interets_bruts, + COALESCE(SUM(r.prelev_sociaux),0) AS prelev_sociaux, + COALESCE(SUM(r.prelev_forfaitaire),0) AS prelev_forfaitaire, + COALESCE(SUM(r.net_recu),0) AS net_recu, + COUNT(*) AS nb_remboursements + FROM remboursements r + WHERE ${invCondBonus} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + `).get(invArg, annee); + + recap.interets_bruts += recapInteretsPlateforme.interets_bruts; + recap.prelev_sociaux += recapInteretsPlateforme.prelev_sociaux; + recap.prelev_forfaitaire += recapInteretsPlateforme.prelev_forfaitaire; + recap.net_recu += recapInteretsPlateforme.net_recu; + recap.nb_remboursements += recapInteretsPlateforme.nb_remboursements; + const pertes = db.prepare(` SELECT i.id, i.nom_projet, p.nom AS plateforme_nom, i.montant_investi, @@ -125,6 +155,21 @@ router.get('/export', (req, res) => { ORDER BY r.date_remb `).all(invArg, annee); + const invCondBonusExp = scopeAll + ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'r.bonus_investisseur_id = ?'; + const interetsPlateformeRows = db.prepare(` + SELECT + '— Intérêts plateforme' || CASE WHEN NULLIF(r.notes, '') IS NOT NULL THEN ' — ' || r.notes ELSE '' END AS nom_projet, + p.nom AS plateforme, + r.date_remb, r.capital, r.interets_bruts, + r.prelev_sociaux, r.prelev_forfaitaire, r.net_recu, r.statut + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + WHERE ${invCondBonusExp} AND substr(r.date_remb,1,4) = ? AND r.type = 'interets_plateforme' + ORDER BY r.date_remb + `).all(invArg, annee); + const corrCond2 = scopeAll ? 'c.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' : 'c.investisseur_id = ?'; @@ -141,7 +186,7 @@ router.get('/export', (req, res) => { `).all(invArg, annee); const header = ['Projet','Plateforme','Date','Capital','Interets bruts','Prelev sociaux','Impot revenu','Net recu','Statut']; - const allRows = [...rows, ...corrRows]; + const allRows = [...rows, ...interetsPlateformeRows, ...corrRows].sort((a, b) => (a.date_remb || '').localeCompare(b.date_remb || '')); const csv = [ header.join(';'), ...allRows.map(r => [ @@ -190,6 +235,52 @@ router.get('/cerfa2561', (req, res) => { ORDER BY p.nom, inv.nom `).all(invArg, annee); + // Intérêts plateforme (pas d'investissement — rattachés via bonus_plateforme_id/ + // 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, + p.nom AS plateforme_nom, + p.domiciliation, + p.fiscalite, + p.type_produit_fiscal, + inv.id AS investisseur_id, + inv.nom AS investisseur_nom, + inv.prenom AS investisseur_prenom, + COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts, + COALESCE(SUM(r.prelev_sociaux), 0) AS prelev_sociaux, + COALESCE(SUM(r.prelev_forfaitaire), 0) AS prelev_forfaitaire + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id + WHERE ${invCondBonus2561} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + GROUP BY p.id, inv.id + `).all(invArg, annee); + + // Fusion des deux sources sur la clé plateforme × investisseur + const rowsMap = {}; + for (const r of rows) rowsMap[`${r.plateforme_id}_${r.investisseur_id}`] = { ...r }; + for (const r of rowsInteretsPlateforme) { + const key = `${r.plateforme_id}_${r.investisseur_id}`; + if (rowsMap[key]) { + rowsMap[key].interets_bruts += r.interets_bruts; + rowsMap[key].prelev_sociaux += r.prelev_sociaux; + rowsMap[key].prelev_forfaitaire += r.prelev_forfaitaire; + } else { + rowsMap[key] = { ...r }; + } + } + const rowsMerged = Object.values(rowsMap).sort((a, b) => + a.plateforme_nom.localeCompare(b.plateforme_nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? '')); + // Pertes en capital par plateforme × investisseur const pertesRows = db.prepare(` SELECT @@ -212,7 +303,7 @@ router.get('/cerfa2561', (req, res) => { pertesMap[`${p.plateforme_id}_${p.investisseur_id}`] = Math.max(0, p.perte_capital); } - const lignes = rows.map(r => { + const lignes = rowsMerged.map(r => { const key = `${r.plateforme_id}_${r.investisseur_id}`; const perteCapital = pertesMap[key] ?? 0; const isFlatTax = r.domiciliation === 'FR' && r.fiscalite === 'flat_tax'; @@ -265,6 +356,23 @@ router.get('/cerfa2561', (req, res) => { GROUP BY p.id, i.investisseur_id, mois `).all(invArg, annee); + const moisRowsInteretsPlateforme = db.prepare(` + SELECT + p.id AS plateforme_id, + r.bonus_investisseur_id AS investisseur_id, + substr(r.date_remb,6,2) AS mois, + COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts, + COALESCE(SUM(r.prelev_sociaux), 0) AS prelev_sociaux, + COALESCE(SUM(r.prelev_forfaitaire), 0) AS prelev_forfaitaire + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + WHERE ${invCondBonus2561} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + GROUP BY p.id, r.bonus_investisseur_id, mois + `).all(invArg, annee); + const moisMap = {}; for (const m of moisRows) { const key = `${m.plateforme_id}_${m.investisseur_id}`; @@ -275,6 +383,16 @@ router.get('/cerfa2561', (req, res) => { prelev_forfaitaire: round2(m.prelev_forfaitaire), }; } + for (const m of moisRowsInteretsPlateforme) { + const key = `${m.plateforme_id}_${m.investisseur_id}`; + if (!moisMap[key]) moisMap[key] = {}; + const existing = moisMap[key][m.mois]; + moisMap[key][m.mois] = { + interets_bruts: round2((existing?.interets_bruts ?? 0) + m.interets_bruts), + prelev_sociaux: round2((existing?.prelev_sociaux ?? 0) + m.prelev_sociaux), + prelev_forfaitaire: round2((existing?.prelev_forfaitaire ?? 0) + m.prelev_forfaitaire), + }; + } const lignesWithMois = lignes.map(l => ({ ...l, @@ -318,7 +436,31 @@ router.get('/cerfa2561/remboursements', (req, res) => { ORDER BY r.date_remb `).all(invArg, ...platArgs, annee); - res.json(rows); + // Intérêts plateforme correspondant à la même plateforme (et investisseur si précisé) + const invCondBonusDetail = scopeAll + ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'r.bonus_investisseur_id = ?'; + const platCondBonus = investisseur_id + ? 'AND p.id = ? AND r.bonus_investisseur_id = ?' + : 'AND p.id = ?'; + const rowsInteretsPlateforme = db.prepare(` + SELECT + r.id, r.date_remb, r.capital, r.interets_bruts, + r.prelev_sociaux, r.prelev_forfaitaire, r.interets_nets, r.net_recu, + r.statut, r.notes, + '— Intérêts plateforme' || CASE WHEN NULLIF(r.notes, '') IS NOT NULL THEN ' — ' || r.notes ELSE '' END AS nom_projet + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + WHERE ${invCondBonusDetail} ${platCondBonus} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + ORDER BY r.date_remb + `).all(invArg, ...platArgs, annee); + + const allRows = [...rows, ...rowsInteretsPlateforme].sort((a, b) => (a.date_remb || '').localeCompare(b.date_remb || '')); + + res.json(allRows); }); @@ -346,7 +488,20 @@ router.get('/years', (req, res) => { ORDER BY annee DESC `).all(...invParams); - res.json(rows.map(r => r.annee)); + const invWhereBonus = scopeAll + ? 'r.bonus_investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' + : 'r.bonus_investisseur_id = ?'; + const rowsInteretsPlateforme = db.prepare(` + SELECT DISTINCT strftime('%Y', r.date_remb) AS annee + FROM remboursements r + WHERE ${invWhereBonus} + AND r.type = 'interets_plateforme' + `).all(...invParams); + + const years = [...new Set([...rows.map(r => r.annee), ...rowsInteretsPlateforme.map(r => r.annee)])] + .sort((a, b) => b.localeCompare(a)); + + res.json(years); }); @@ -375,6 +530,30 @@ router.get('/2778', (req, res) => { ORDER BY p.nom, inv.nom `).all(invArg, annee); + // 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 + JOIN plateformes p ON p.id = r.bonus_plateforme_id + JOIN investisseurs inv ON inv.id = r.bonus_investisseur_id + WHERE ${invCondBonus2778} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + AND p.domiciliation != 'FR' + `).all(invArg, annee); + + const platRowsMap = {}; + for (const p of [...platRows, ...platRowsInteretsPlateforme]) { + platRowsMap[`${p.id}_${p.investisseur_id}`] = p; + } + const platRowsMerged = Object.values(platRowsMap).sort((a, b) => + a.nom.localeCompare(b.nom) || (a.investisseur_nom ?? '').localeCompare(b.investisseur_nom ?? '')); + // Montants mensuels par plateforme × investisseur const moisRows = db.prepare(` SELECT @@ -398,14 +577,40 @@ router.get('/2778', (req, res) => { GROUP BY p.id, i.investisseur_id, mois `).all(invArg, annee); + const moisRowsInteretsPlateforme = db.prepare(` + SELECT + p.id AS plateforme_id, + r.bonus_investisseur_id AS investisseur_id, + substr(r.date_remb,6,2) AS mois, + COALESCE(SUM( + CASE WHEN r.interets_bruts_avant_local IS NOT NULL AND r.interets_bruts_avant_local > 0 + THEN r.interets_bruts_avant_local + ELSE r.interets_bruts + END + ), 0) AS montant + FROM remboursements r + JOIN plateformes p ON p.id = r.bonus_plateforme_id + WHERE ${invCondBonus2778} + AND substr(r.date_remb,1,4) = ? + AND r.statut IN ('paye','partiel') + AND r.type = 'interets_plateforme' + AND p.domiciliation != 'FR' + GROUP BY p.id, r.bonus_investisseur_id, mois + `).all(invArg, annee); + const moisMap = {}; for (const m of moisRows) { const key = `${m.plateforme_id}_${m.investisseur_id}`; if (!moisMap[key]) moisMap[key] = {}; moisMap[key][m.mois] = round2(m.montant); } + for (const m of moisRowsInteretsPlateforme) { + const key = `${m.plateforme_id}_${m.investisseur_id}`; + if (!moisMap[key]) moisMap[key] = {}; + moisMap[key][m.mois] = round2((moisMap[key][m.mois] ?? 0) + m.montant); + } - const plateformes = platRows.map(p => ({ + const plateformes = platRowsMerged.map(p => ({ id: `${p.id}_${p.investisseur_id}`, plateforme_id: p.id, nom: p.nom, diff --git a/frontend/src/components/Cerfa2778Preview.jsx b/frontend/src/components/Cerfa2778Preview.jsx index f1fef49..322a78a 100644 --- a/frontend/src/components/Cerfa2778Preview.jsx +++ b/frontend/src/components/Cerfa2778Preview.jsx @@ -385,7 +385,7 @@ export default function Cerfa2778Preview({ annee, activeView }) { {/* Détail des plateformes incluses */} - {moisData.detail.length > 1 && ( + {moisData.detail.length > 0 && (
Plateformes incluses