Files
crowdlending-app/backend/src/routes/taxreport.js
T
Olivier CROGUENNEC 48ed7fe65e Initial commit
2026-06-13 14:57:15 +02:00

422 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;