import { Router } from 'express'; import db from '../db/index.js'; import { requireInvestisseur } from '../middleware/investisseurScope.js'; const router = Router(); router.use(requireInvestisseur); const round2 = v => Math.round((v ?? 0) * 100) / 100; router.get('/', (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 recap = 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 JOIN investissements i ON i.id = r.investissement_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') `).get(invArg, annee); const pertes = db.prepare(` SELECT i.id, i.nom_projet, p.nom AS plateforme_nom, i.montant_investi, COALESCE((SELECT SUM(r.capital) FROM remboursements r WHERE r.investissement_id = i.id),0) AS capital_rembourse, (i.montant_investi - COALESCE((SELECT SUM(r.capital) FROM remboursements r WHERE r.investissement_id = i.id),0) ) AS perte_capital, i.statut, i.updated_at FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND i.statut IN ('en_retard','cloture') AND substr(i.updated_at,1,4) = ? `).all(invArg, annee); const pertesTotales = pertes.reduce((s, p) => s + Math.max(0, p.perte_capital), 0); const detail = db.prepare(` SELECT i.id AS investissement_id, i.nom_projet, p.nom AS plateforme_nom, inv.nom AS investisseur_nom, SUM(r.interets_bruts) AS interets_bruts, SUM(r.prelev_sociaux) AS prelev_sociaux, SUM(r.prelev_forfaitaire) AS prelev_forfaitaire, SUM(r.net_recu) AS net_recu, COUNT(r.id) AS nb_echeances FROM remboursements r JOIN investissements i ON i.id = r.investissement_id JOIN investisseurs inv ON inv.id = i.investisseur_id JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') GROUP BY i.id ORDER BY interets_bruts DESC `).all(invArg, annee); const corrCond = scopeAll ? 'c.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)' : 'c.investisseur_id = ?'; const corrections = db.prepare(` SELECT c.id, c.date, c.montant, c.notes, p.nom AS plateforme_nom, inv.nom AS investisseur_nom FROM corrections_solde c JOIN plateformes p ON p.id = c.plateforme_id JOIN investisseurs inv ON inv.id = c.investisseur_id WHERE ${corrCond} AND substr(c.date,1,4) = ? ORDER BY c.date DESC `).all(invArg, annee); const totalCorrections = corrections.reduce((s, c) => s + c.montant, 0); const interetsNets = recap.interets_bruts - recap.prelev_sociaux - recap.prelev_forfaitaire; const recapTotal = { interets_bruts: round2(recap.interets_bruts), prelev_sociaux: round2(recap.prelev_sociaux), prelev_forfaitaire: round2(recap.prelev_forfaitaire), interets_nets: round2(interetsNets), interets_nets_corriges: round2(interetsNets + totalCorrections), nb_remboursements: recap.nb_remboursements, total_corrections: round2(totalCorrections), }; const cases = { case_2TR: round2(recap.interets_bruts), case_2CK: round2(recap.prelev_forfaitaire), case_2BH: round2(recap.interets_bruts), case_2BH_perte_capital: round2(pertesTotales), note: "Les cases sont indicatives. Verifiez avec votre situation fiscale reelle.", }; res.json({ annee, recap: recapTotal, pertes, pertesTotales: round2(pertesTotales), detail, corrections, cases }); }); router.get('/export', (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(` SELECT i.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 investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? 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 = ?'; const corrRows = db.prepare(` SELECT c.notes AS nom_projet, p.nom AS plateforme, c.date AS date_remb, 0 AS capital, 0 AS interets_bruts, 0 AS prelev_sociaux, 0 AS prelev_forfaitaire, c.montant AS net_recu, 'correction_solde' AS statut FROM corrections_solde c JOIN plateformes p ON p.id = c.plateforme_id WHERE ${corrCond2} AND substr(c.date,1,4) = ? ORDER BY c.date `).all(invArg, annee); const header = ['Projet','Plateforme','Date','Capital','Interets bruts','Prelev sociaux','Impot revenu','Net recu','Statut']; const allRows = [...rows, ...corrRows]; const csv = [ header.join(';'), ...allRows.map(r => [ esc(r.nom_projet), esc(r.plateforme), r.date_remb, r.capital, r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire, r.net_recu, r.statut, ].join(';')), ].join('\n'); res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="2778-SD-${annee}.csv"`); 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; const rows = 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 investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id JOIN investisseurs inv ON inv.id = i.investisseur_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') AND r.type = 'normal' GROUP BY p.id, inv.id ORDER BY p.nom, inv.nom `).all(invArg, annee); // Pertes en capital par plateforme × investisseur const pertesRows = db.prepare(` SELECT p.id AS plateforme_id, i.investisseur_id, COALESCE(SUM( i.montant_investi - COALESCE((SELECT SUM(r2.capital) FROM remboursements r2 WHERE r2.investissement_id = i.id),0) ), 0) AS perte_capital FROM investissements i JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND i.statut IN ('en_retard','cloture') AND substr(i.updated_at,1,4) = ? GROUP BY p.id, i.investisseur_id `).all(invArg, annee); const pertesMap = {}; for (const p of pertesRows) { pertesMap[`${p.plateforme_id}_${p.investisseur_id}`] = Math.max(0, p.perte_capital); } const lignes = rows.map(r => { const key = `${r.plateforme_id}_${r.investisseur_id}`; const perteCapital = pertesMap[key] ?? 0; const isFlatTax = r.domiciliation === 'FR' && r.fiscalite === 'flat_tax'; const use2TR = r.type_produit_fiscal === '2TR'; const interetsNets = round2(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire); return { annee, plateforme_id: r.plateforme_id, plateforme_nom: r.plateforme_nom, domiciliation: r.domiciliation, fiscalite: r.fiscalite, type_produit_fiscal: r.type_produit_fiscal, investisseur_id: r.investisseur_id, investisseur_nom: r.investisseur_nom, investisseur_prenom: r.investisseur_prenom, interets_bruts: round2(r.interets_bruts), prelev_sociaux: round2(r.prelev_sociaux), prelev_forfaitaire: round2(r.prelev_forfaitaire), interets_nets: interetsNets, // Cases fiscales — d'après IFU réels : // 2TT ou 2TR = intérêts bruts (toutes plateformes) // 2BH = intérêts bruts si PS déjà prélevés (prelev_sociaux > 0) // 2CK = PFNL déjà versé (flat-tax FR uniquement) // 2TY = pertes en capital case_2TT: !use2TR ? Math.round(r.interets_bruts) : 0, case_2TR: use2TR ? Math.round(r.interets_bruts) : 0, case_2BH: r.prelev_sociaux > 0 ? Math.round(r.interets_bruts) : 0, case_2CK: isFlatTax ? Math.round(r.prelev_forfaitaire) : 0, case_2TY: perteCapital > 0 ? Math.round(perteCapital) : 0, }; }); // Breakdown mensuel par plateforme × investisseur const moisRows = db.prepare(` SELECT p.id AS plateforme_id, i.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 investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') AND r.type = 'normal' GROUP BY p.id, i.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] = { interets_bruts: round2(m.interets_bruts), prelev_sociaux: round2(m.prelev_sociaux), prelev_forfaitaire: round2(m.prelev_forfaitaire), }; } const lignesWithMois = lignes.map(l => ({ ...l, mois: moisMap[`${l.plateforme_id}_${l.investisseur_id}`] ?? {}, })); res.json({ annee, lignes: lignesWithMois }); }); /* ── CERFA 2561 — détail des remboursements par plateforme × investisseur ── */ router.get('/cerfa2561/remboursements', (req, res) => { const { annee, plateforme_id, investisseur_id } = req.query; if (!annee || !plateforme_id) return res.status(400).json({ error: 'annee et plateforme_id requis' }); 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 platCond = investisseur_id ? 'AND p.id = ? AND i.investisseur_id = ?' : 'AND p.id = ?'; const platArgs = investisseur_id ? [Number(plateforme_id), Number(investisseur_id)] : [Number(plateforme_id)]; const rows = 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, i.nom_projet FROM remboursements r JOIN investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} ${platCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') AND r.type = 'normal' ORDER BY r.date_remb `).all(invArg, ...platArgs, annee); res.json(rows); }); /* ── Années disponibles ── */ router.get('/years', (req, res) => { const scopeAll = req.query.scope === 'all'; let invWhere, invParams; if (scopeAll) { invWhere = 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)'; invParams = [req.user.id]; } else { const invId = Number(req.header('X-Investisseur-Id')); if (!invId) return res.status(400).json({ error: 'Missing investisseur id' }); invWhere = 'i.investisseur_id = ?'; invParams = [invId]; } const rows = db.prepare(` SELECT DISTINCT strftime('%Y', r.date_remb) AS annee FROM remboursements r JOIN investissements i ON i.id = r.investissement_id WHERE ${invWhere} AND r.type = 'normal' ORDER BY annee DESC `).all(...invParams); res.json(rows.map(r => r.annee)); }); /* ── 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; // 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 FROM remboursements r JOIN investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id JOIN investisseurs inv ON inv.id = i.investisseur_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') AND r.type = 'normal' AND p.domiciliation != 'FR' ORDER BY p.nom, inv.nom `).all(invArg, annee); // Montants mensuels par plateforme × investisseur const moisRows = db.prepare(` SELECT p.id AS plateforme_id, i.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 investissements i ON i.id = r.investissement_id JOIN plateformes p ON p.id = i.plateforme_id WHERE ${invCond} AND substr(r.date_remb,1,4) = ? AND r.statut IN ('paye','partiel') AND r.type = 'normal' AND p.domiciliation != 'FR' GROUP BY p.id, i.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); } const plateformes = platRows.map(p => ({ id: `${p.id}_${p.investisseur_id}`, plateforme_id: p.id, nom: p.nom, investisseur_id: p.investisseur_id, investisseur_nom: p.investisseur_nom, investisseur_prenom: p.investisseur_prenom, mois: moisMap[`${p.id}_${p.investisseur_id}`] ?? {}, })); res.json({ annee, plateformes }); }); export default router;