Initial commit

This commit is contained in:
Olivier CROGUENNEC committed 2026-06-13 14:57:15 +02:00
commit 48ed7fe65e
209 files changed
+49979

No files matched your search

+884
View File
@@ -0,0 +1,884 @@
import { Router } from 'express';
import db from '../db/index.js';
const router = Router();
/**
* GET /api/dashboard
*
* ?scope=all → agrège tous les investisseurs de l'utilisateur
* (défaut) → filtre sur l'investisseur donné par X-Investisseur-Id
*/
router.get('/', (req, res) => {
const scopeAll = req.query.scope === 'all';
const userId = req.user.id;
/* ── Résolution de l'investisseur cible ─────────────────────── */
let invWhere, invParams, invId;
if (scopeAll) {
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
} else {
const raw = req.header('X-Investisseur-Id');
invId = Number(raw);
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
invWhere = 'investisseur_id = ?';
invParams = [invId];
}
/* ── Cash global ─────────────────────────────────────────────── */
const cash = db.prepare(`
SELECT
COALESCE(SUM(CASE WHEN type='depot' THEN montant END),0) AS total_depots,
COALESCE(SUM(CASE WHEN type='retrait' THEN montant END),0) AS total_retraits
FROM depots_retraits WHERE ${invWhere}
`).get(...invParams);
/* ── Cash par plateforme ─────────────────────────────────────── */
const cashByPlatform = db.prepare(`
SELECT p.id AS plateforme_id, p.nom,
plat_inv.nom AS detenteur_nom,
COALESCE(SUM(CASE WHEN dr.type='depot' THEN dr.montant END),0) AS depots,
COALESCE(SUM(CASE WHEN dr.type='retrait' THEN dr.montant END),0) AS retraits,
COALESCE(SUM(CASE WHEN dr.type='depot' THEN dr.montant
WHEN dr.type='retrait' THEN -dr.montant END),0) AS solde_net
FROM plateformes p
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
LEFT JOIN depots_retraits dr ON dr.plateforme_id = p.id AND ${invWhere.replace('investisseur_id', 'dr.investisseur_id')}
WHERE p.user_id = ?
GROUP BY p.id, p.nom, plat_inv.nom
ORDER BY p.nom
`).all(...invParams, userId);
/* ── Solde porte-monnaie par plateforme ──────────────────────── */
const drManuelPerPlat = db.prepare(`
SELECT plateforme_id,
COALESCE(SUM(CASE WHEN type='depot' THEN montant ELSE 0 END), 0) AS depots,
COALESCE(SUM(CASE WHEN type='retrait' AND COALESCE(source,'') != 'auto_remboursement'
THEN montant ELSE 0 END), 0) AS retraits_manuels
FROM depots_retraits
WHERE ${invWhere}
GROUP BY plateforme_id
`).all(...invParams);
const rembWalletPerPlat = db.prepare(`
SELECT i.plateforme_id,
COALESCE(SUM(
CASE p.fiscalite
WHEN 'flat_tax' THEN r.net_recu
-- Plateformes hors France : pas de PFU prélevé à la source.
-- interets_bruts est déjà net de la retenue locale si applicable.
ELSE r.capital + r.cashback + r.interets_bruts
END
), 0) AS remb_wallet
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
JOIN plateformes p ON p.id = i.plateforme_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND r.type = 'normal'
AND r.methode_remboursement = 'portefeuille'
GROUP BY i.plateforme_id
`).all(...invParams);
const bonusWalletPerPlat = db.prepare(`
SELECT bonus_plateforme_id AS plateforme_id,
COALESCE(SUM(cashback), 0) AS bonus_wallet
FROM remboursements
WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
AND type IN ('bonus_parrainage', 'bonus_plateforme')
GROUP BY bonus_plateforme_id
`).all(...invParams);
const capitalInvestiPerPlat = db.prepare(`
SELECT i.plateforme_id,
COALESCE(SUM(
i.montant_investi
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
), 0) AS capital_investi
FROM investissements i
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
GROUP BY i.plateforme_id
`).all(...invParams);
// Corrections de solde (micro-ecarts fiscaux sur plateformes flat_tax)
const correctionsPerPlat = db.prepare(`
SELECT plateforme_id,
COALESCE(SUM(montant), 0) AS total_correction
FROM corrections_solde
WHERE ${invWhere}
GROUP BY plateforme_id
`).all(...invParams);
// Fusion en une map plateforme_id → solde_portefeuille
const walletMap = {};
for (const r of drManuelPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.depots - r.retraits_manuels;
for (const r of rembWalletPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.remb_wallet;
for (const r of bonusWalletPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.bonus_wallet;
for (const r of capitalInvestiPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) - r.capital_investi;
for (const r of correctionsPerPlat) walletMap[r.plateforme_id] = (walletMap[r.plateforme_id] || 0) + r.total_correction;
const cashByPlatformEnriched = cashByPlatform.map(p => ({
...p,
solde_portefeuille: Math.round((walletMap[p.plateforme_id] || 0) * 100) / 100,
}));
const solde_portefeuille_total = Math.round(
cashByPlatformEnriched.reduce((s, p) => s + p.solde_portefeuille, 0) * 100
) / 100;
/* ── Portfolio ───────────────────────────────────────────────── */
const portfolio = db.prepare(`
SELECT
COUNT(*) AS nb_projets,
COALESCE(SUM(i.montant_investi),0) AS total_investi,
COALESCE(SUM(CASE WHEN i.statut='en_cours' THEN
i.montant_investi
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type = 'normal'), 0)
END), 0) AS encours,
COALESCE(SUM(CASE WHEN i.statut='rembourse' THEN i.montant_investi END),0) AS rembourse,
COALESCE(SUM(CASE WHEN i.statut IN ('en_retard','procedure') THEN
i.montant_investi
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
- COALESCE((SELECT SUM(rb.capital) FROM remboursements rb WHERE rb.investissement_id = i.id AND rb.type = 'normal'), 0)
END), 0) AS en_defaut
FROM investissements i WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
`).get(...invParams);
/* ── Interets ────────────────────────────────────────────────── */
const interets = 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
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
`).get(...invParams);
/* ── Interets par annee ──────────────────────────────────────── */
const interetsParAnnee = scopeAll
? db.prepare(`
SELECT annee,
SUM(interets_bruts) AS interets_bruts,
SUM(prelev_sociaux) AS prelev_sociaux,
SUM(prelev_forfaitaire) AS prelev_forfaitaire,
SUM(net_recu) AS net_recu
FROM v_interets_annuels
WHERE investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)
GROUP BY annee ORDER BY annee DESC
`).all(userId)
: db.prepare(`
SELECT * FROM v_interets_annuels WHERE investisseur_id = ? ORDER BY annee DESC
`).all(invId);
/* ── Prochaines echeances ────────────────────────────────────── */
const upcoming = db.prepare(`
SELECT s.*, i.nom_projet, p.nom AS plateforme_nom,
plat_inv.nom AS plateforme_detenteur_nom,
(SELECT r.date_remb FROM remboursements r
WHERE r.investissement_id = s.investissement_id
AND r.date_remb = s.date_prevue
LIMIT 1) AS remb_date_exact,
(SELECT r.date_remb FROM remboursements r
WHERE r.investissement_id = s.investissement_id
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
AND r.date_remb != s.date_prevue
LIMIT 1) AS remb_date_mois
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
JOIN plateformes p ON p.id = i.plateforme_id
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND s.date_prevue >= date('now','start of month')
AND s.date_prevue < date('now','start of month','+1 month')
ORDER BY s.date_prevue
LIMIT 50
`).all(...invParams);
/* ── En retard / procedure ───────────────────────────────────── */
const enDefaut = db.prepare(`
SELECT i.*, p.nom AS plateforme_nom,
plat_inv.nom AS plateforme_detenteur_nom
FROM investissements i
JOIN plateformes p ON p.id = i.plateforme_id
LEFT JOIN investisseurs plat_inv ON plat_inv.id = p.investisseur_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND i.statut IN ('en_retard','procedure')
ORDER BY i.date_souscription DESC
`).all(...invParams);
res.json({ cash, cashByPlatform: cashByPlatformEnriched, solde_portefeuille_total, portfolio, interets, interetsParAnnee, upcoming, enDefaut });
});
/**
* GET /api/dashboard/solde-historique
*
* Retourne le solde porte-monnaie calculé pour une plateforme à une date donnée.
* Utilise la même formule que solde_portefeuille mais filtrée par date <= :date.
*
* Query params : plateforme_id (requis), date YYYY-MM-DD (requis)
*/
router.get('/solde-historique', (req, res) => {
const { plateforme_id, date } = req.query;
if (!plateforme_id || !date) {
return res.status(400).json({ error: 'plateforme_id et date sont requis' });
}
const userId = req.user.id;
const platId = Number(plateforme_id);
// Vérifier que la plateforme appartient à l'utilisateur
const plat = db.prepare('SELECT id FROM plateformes WHERE id = ? AND user_id = ?').get(platId, userId);
if (!plat) return res.status(403).json({ error: 'Plateforme non trouvée' });
/* ── Résolution du scope investisseur (même logique que GET /) ── */
let invWhere, invParams;
const raw = req.header('X-Investisseur-Id');
const invId = Number(raw);
if (invId) {
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur non trouvé' });
invWhere = 'investisseur_id = ?';
invParams = [invId];
} else {
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
}
/* ── 1. Dépôts − retraits manuels (jusqu'à :date) ─────────────── */
const dr = db.prepare(`
SELECT
COALESCE(SUM(CASE WHEN type='depot' THEN montant ELSE 0 END), 0) AS depots,
COALESCE(SUM(CASE WHEN type='retrait'
AND COALESCE(source,'') != 'auto_remboursement'
THEN montant ELSE 0 END), 0) AS retraits_manuels
FROM depots_retraits
WHERE plateforme_id = ? AND ${invWhere} AND date_operation <= ?
`).get(platId, ...invParams, date);
/* ── 2. Remboursements vers porte-monnaie (jusqu'à :date) ──────── */
const remb = db.prepare(`
SELECT COALESCE(SUM(
CASE p.fiscalite
WHEN 'flat_tax' THEN r.net_recu
ELSE r.capital + r.cashback + r.interets_bruts
END
), 0) AS remb_wallet
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
JOIN plateformes p ON p.id = i.plateforme_id
WHERE i.plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND r.type = 'normal' AND r.methode_remboursement = 'portefeuille'
AND r.date_remb <= ?
`).get(platId, ...invParams, date);
/* ── 3. Bonus porte-monnaie (jusqu'à :date) ────────────────────── */
const bonus = db.prepare(`
SELECT COALESCE(SUM(cashback), 0) AS bonus_wallet
FROM remboursements
WHERE bonus_plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
AND type IN ('bonus_parrainage', 'bonus_plateforme')
AND date_remb <= ?
`).get(platId, ...invParams, date);
/* ── 4. Capital investi (souscriptions + réinvestissements jusqu'à :date) ── */
const capital = db.prepare(`
SELECT COALESCE(SUM(
i.montant_investi
+ COALESCE((
SELECT SUM(rv.montant)
FROM reinvestissements rv
WHERE rv.investissement_id = i.id
AND rv.date_reinvestissement <= ?
), 0)
), 0) AS capital_investi
FROM investissements i
WHERE i.plateforme_id = ? AND ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND i.date_souscription <= ?
`).get(date, platId, ...invParams, date);
/* ── 5. Corrections de solde (jusqu'à :date) ───────────────────── */
const corrections = db.prepare(`
SELECT COALESCE(SUM(montant), 0) AS total_correction
FROM corrections_solde
WHERE plateforme_id = ? AND ${invWhere} AND date <= ?
`).get(platId, ...invParams, date);
const solde = Math.round((
dr.depots
- dr.retraits_manuels
+ remb.remb_wallet
+ bonus.bonus_wallet
- capital.capital_investi
+ corrections.total_correction
) * 100) / 100;
res.json({ solde });
});
/**
* GET /api/dashboard/interets-mensuels
*
* Retourne les intérêts réels (remboursements) et projetés (simul_remboursements)
* groupés par mois YYYY-MM pour l'année demandée.
*
* Query params : annee (int, défaut = année courante), scope=all (optionnel)
*/
router.get('/interets-mensuels', (req, res) => {
const annee = parseInt(req.query.annee) || new Date().getFullYear();
const scopeAll = req.query.scope === 'all';
const userId = req.user.id;
const anneeStr = String(annee);
let invWhere, invParams, invId;
if (scopeAll) {
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
} else {
const raw = req.header('X-Investisseur-Id');
invId = Number(raw);
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
invWhere = 'investisseur_id = ?';
invParams = [invId];
}
/* ── Intérêts réels groupés par mois ─────────────────────────── */
// Inclut les remboursements normaux (via investissement) ET les bonus (via bonus_investisseur_id)
let rembourses;
if (scopeAll) {
rembourses = db.prepare(`
SELECT
strftime('%Y-%m', r.date_remb) AS mois,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
COALESCE(SUM(r.capital), 0) AS capital,
COALESCE(SUM(r.cashback), 0) AS cashback
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 strftime('%Y', r.date_remb) = ?
AND (
(r.investissement_id IS NOT NULL AND own.user_id = ?)
OR (r.bonus_investisseur_id IS NOT NULL AND bonus_own.user_id = ?)
)
GROUP BY mois
ORDER BY mois
`).all(anneeStr, userId, userId);
} else {
rembourses = db.prepare(`
SELECT
strftime('%Y-%m', r.date_remb) AS mois,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
COALESCE(SUM(r.capital), 0) AS capital,
COALESCE(SUM(r.cashback), 0) AS cashback
FROM remboursements r
LEFT JOIN investissements inv ON inv.id = r.investissement_id
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
WHERE strftime('%Y', r.date_remb) = ?
AND (
(r.investissement_id IS NOT NULL AND inv.investisseur_id = ?)
OR (r.bonus_investisseur_id IS NOT NULL AND r.bonus_investisseur_id = ?)
)
GROUP BY mois
ORDER BY mois
`).all(anneeStr, invId, invId);
}
/* ── Projections groupées par mois ───────────────────────────── */
// Exclure les investissements qui ont déjà un remboursement réel dans le même mois
// (évite le double-comptage pour le mois en cours)
const projections = db.prepare(`
SELECT
strftime('%Y-%m', s.date_prevue) AS mois,
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND strftime('%Y', s.date_prevue) = ?
AND NOT EXISTS (
SELECT 1 FROM remboursements r
WHERE r.investissement_id = s.investissement_id
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
)
GROUP BY mois
ORDER BY mois
`).all(...invParams, anneeStr);
/* ── Années disponibles (remboursements + projections) ─── */
const annees = db.prepare(`
SELECT DISTINCT CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
UNION
SELECT DISTINCT CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND CAST(strftime('%Y', s.date_prevue) AS INTEGER) <= CAST(strftime('%Y', 'now') AS INTEGER) + 10
ORDER BY annee
`).all(...invParams, ...invParams).map(r => r.annee);
res.json({ rembourses, projections, annees });
});
/**
* GET /api/dashboard/interets-annuels
*
* Retourne les intérêts réels et projetés groupés par ANNÉE (toutes années).
* Les projections excluent les échéances déjà passées (>= début du mois courant).
*
* Query params : scope=all (optionnel)
*/
router.get('/interets-annuels', (req, res) => {
const scopeAll = req.query.scope === 'all';
const userId = req.user.id;
let invWhere, invParams, invId;
if (scopeAll) {
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
} else {
const raw = req.header('X-Investisseur-Id');
invId = Number(raw);
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
invWhere = 'investisseur_id = ?';
invParams = [invId];
}
/* ── Intérêts réels groupés par année ────────────────────────── */
let rembourses;
if (scopeAll) {
rembourses = db.prepare(`
SELECT
CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
COALESCE(SUM(r.capital), 0) AS capital,
COALESCE(SUM(r.cashback), 0) AS cashback
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 (
(r.investissement_id IS NOT NULL AND own.user_id = ?)
OR (r.bonus_investisseur_id IS NOT NULL AND bonus_own.user_id = ?)
)
GROUP BY annee
ORDER BY annee
`).all(userId, userId);
} else {
rembourses = db.prepare(`
SELECT
CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
COALESCE(SUM(r.capital), 0) AS capital,
COALESCE(SUM(r.cashback), 0) AS cashback
FROM remboursements r
LEFT JOIN investissements inv ON inv.id = r.investissement_id
LEFT JOIN investisseurs bonus_own ON bonus_own.id = r.bonus_investisseur_id
WHERE (
(r.investissement_id IS NOT NULL AND inv.investisseur_id = ?)
OR (r.bonus_investisseur_id IS NOT NULL AND r.bonus_investisseur_id = ?)
)
GROUP BY annee
ORDER BY annee
`).all(invId, invId);
}
/* ── Projections groupées par année (uniquement futures/en cours) */
const projections = db.prepare(`
SELECT
CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee,
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND s.date_prevue >= date('now', 'start of month')
GROUP BY annee
ORDER BY annee
`).all(...invParams);
/* ── Années disponibles (remboursements + projections) ───────── */
const annees = db.prepare(`
SELECT DISTINCT CAST(strftime('%Y', r.date_remb) AS INTEGER) AS annee
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
UNION
SELECT DISTINCT CAST(strftime('%Y', s.date_prevue) AS INTEGER) AS annee
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND CAST(strftime('%Y', s.date_prevue) AS INTEGER) <= CAST(strftime('%Y', 'now') AS INTEGER) + 10
ORDER BY annee
`).all(...invParams, ...invParams).map(r => r.annee);
/* ── Capital souscrit par année (pour KPI "Capital investi" filtré) ── */
const capitalParAnnee = db.prepare(`
SELECT
CAST(strftime('%Y', i.date_souscription) AS INTEGER) AS annee,
COALESCE(SUM(
i.montant_investi
+ COALESCE((SELECT SUM(rv.montant) FROM reinvestissements rv WHERE rv.investissement_id = i.id), 0)
), 0) AS capital_souscrit
FROM investissements i
WHERE ${invWhere}
GROUP BY annee
ORDER BY annee
`).all(...invParams);
res.json({ rembourses, projections, annees, capitalParAnnee });
});
/**
* GET /api/dashboard/interets-par-plateforme
*
* Retourne les intérêts réels + projetés groupés par plateforme et par mois
* pour l'année demandée, ainsi que le capital investi estimé chaque mois.
*
* Query params : annee (int, défaut = année courante), scope=all (optionnel)
*/
router.get('/interets-par-plateforme', (req, res) => {
const annee = parseInt(req.query.annee) || new Date().getFullYear();
const scopeAll = req.query.scope === 'all';
const userId = req.user.id;
const anneeStr = String(annee);
let invWhere, invParams, invId;
if (scopeAll) {
invWhere = 'investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
} else {
const raw = req.header('X-Investisseur-Id');
invId = Number(raw);
if (!invId) return res.status(400).json({ error: 'Missing investisseur id (header X-Investisseur-Id)' });
const row = db.prepare('SELECT id FROM investisseurs WHERE id = ? AND user_id = ?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur not found or not owned by user' });
invWhere = 'investisseur_id = ?';
invParams = [invId];
}
/* ── Intérêts réels par plateforme et par mois ───────────────── */
const remboursesByPlat = db.prepare(`
SELECT
p.id AS plateforme_id,
p.nom AS plateforme_nom,
strftime('%Y-%m', r.date_remb) AS mois,
COALESCE(SUM(r.interets_bruts), 0) AS interets_bruts,
COALESCE(SUM(r.interets_bruts - r.prelev_sociaux - r.prelev_forfaitaire), 0) AS interets_nets,
COALESCE(SUM(r.cashback), 0) AS cashback,
COALESCE(SUM(r.capital), 0) AS capital
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
JOIN plateformes p ON p.id = i.plateforme_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND strftime('%Y', r.date_remb) = ?
AND r.type = 'normal'
GROUP BY p.id, p.nom, mois
ORDER BY p.nom, mois
`).all(...invParams, anneeStr);
/* ── Cashback bonus (parrainage / plateforme) par plateforme et par mois ── */
const bonusCashbackByPlat = db.prepare(`
SELECT
r.bonus_plateforme_id AS plateforme_id,
p.nom AS plateforme_nom,
strftime('%Y-%m', r.date_remb) AS mois,
COALESCE(SUM(r.cashback), 0) AS cashback
FROM remboursements r
JOIN plateformes p ON p.id = r.bonus_plateforme_id
WHERE ${invWhere.replace('investisseur_id', 'bonus_investisseur_id')}
AND r.type IN ('bonus_parrainage', 'bonus_plateforme')
AND strftime('%Y', r.date_remb) = ?
GROUP BY r.bonus_plateforme_id, p.nom, mois
ORDER BY p.nom, mois
`).all(...invParams, anneeStr);
/* ── Projections par plateforme et par mois ──────────────────── */
// Exclure les investissements qui ont déjà un remboursement réel dans le même mois
const projectionsByPlat = db.prepare(`
SELECT
p.id AS plateforme_id,
p.nom AS plateforme_nom,
strftime('%Y-%m', s.date_prevue) AS mois,
COALESCE(SUM(s.interets_prevus), 0) AS interets_prevus,
COALESCE(SUM(s.capital_prevu), 0) AS capital_prevu
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
JOIN plateformes p ON p.id = i.plateforme_id
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
AND strftime('%Y', s.date_prevue) = ?
AND NOT EXISTS (
SELECT 1 FROM remboursements r
WHERE r.investissement_id = s.investissement_id
AND strftime('%Y-%m', r.date_remb) = strftime('%Y-%m', s.date_prevue)
)
GROUP BY p.id, p.nom, mois
ORDER BY p.nom, mois
`).all(...invParams, anneeStr);
/* ── Capital investi mensuel ─────────────────────────────────── */
// Récupérer tous les investissements avec leurs dates pour calculer
// le capital restant dû chaque mois (montant_investi + reinvests − capital_remboursé_cumulé)
const invs = db.prepare(`
SELECT
i.id,
i.montant_investi,
i.date_souscription,
i.date_cible,
i.statut,
(SELECT MAX(r.date_remb) FROM remboursements r WHERE r.investissement_id = i.id) AS last_remb_date
FROM investissements i
WHERE ${invWhere.replace('investisseur_id', 'i.investisseur_id')}
`).all(...invParams);
const invIds = invs.map(i => i.id);
const placeholder = invIds.map(() => '?').join(',');
// Remboursements de capital par investissement avec leur date (tous types normaux)
const capitalRembRows = invIds.length ? db.prepare(`
SELECT investissement_id, date_remb, SUM(capital) AS capital_remb
FROM remboursements
WHERE investissement_id IN (${placeholder})
AND type = 'normal'
GROUP BY investissement_id, date_remb
`).all(...invIds) : [];
// Réinvestissements par investissement avec leur date
const reinvestRows = invIds.length ? db.prepare(`
SELECT investissement_id, date_reinvestissement, SUM(montant) AS montant
FROM reinvestissements
WHERE investissement_id IN (${placeholder})
GROUP BY investissement_id, date_reinvestissement
`).all(...invIds) : [];
// Capital projeté par investissement (simul_remboursements) — pour les mois futurs
const capitalSimulRows = invIds.length ? db.prepare(`
SELECT investissement_id, date_prevue, SUM(capital_prevu) AS capital_prevu
FROM simul_remboursements
WHERE investissement_id IN (${placeholder})
GROUP BY investissement_id, date_prevue
`).all(...invIds) : [];
// Map : inv_id → [{date, capital}]
const capitalRembMap = {};
for (const r of capitalRembRows) {
if (!capitalRembMap[r.investissement_id]) capitalRembMap[r.investissement_id] = [];
capitalRembMap[r.investissement_id].push({ date: r.date_remb, capital: r.capital_remb });
}
// Map : inv_id → [{date, montant}]
const reinvestMap = {};
for (const r of reinvestRows) {
if (!reinvestMap[r.investissement_id]) reinvestMap[r.investissement_id] = [];
reinvestMap[r.investissement_id].push({ date: r.date_reinvestissement, montant: r.montant });
}
// Map : inv_id → [{date, capital}]
const capitalSimulMap = {};
for (const r of capitalSimulRows) {
if (!capitalSimulMap[r.investissement_id]) capitalSimulMap[r.investissement_id] = [];
capitalSimulMap[r.investissement_id].push({ date: r.date_prevue, capital: r.capital_prevu });
}
// Date du jour au format YYYY-MM-DD (locale) pour distinguer passé/futur
const _today = new Date();
const todayStr = `${_today.getFullYear()}-${String(_today.getMonth() + 1).padStart(2, '0')}-${String(_today.getDate()).padStart(2, '0')}`;
const capitalMensuel = [];
for (let m = 1; m <= 12; m++) {
const moisStr = `${anneeStr}-${String(m).padStart(2, '0')}`;
const firstDay = `${moisStr}-01`;
// Dernier jour du mois (calcul local pour éviter le décalage UTC)
const _d = new Date(annee, m, 0);
const lastDay = `${_d.getFullYear()}-${String(_d.getMonth() + 1).padStart(2, '0')}-${String(_d.getDate()).padStart(2, '0')}`;
let capital = 0;
let enDefaut = 0;
for (const inv of invs) {
if (inv.date_souscription > lastDay) continue; // pas encore débuté
// Capital réellement remboursé jusqu'à la fin de ce mois
const rembs = capitalRembMap[inv.id] ?? [];
const capitalRembCumul = rembs
.filter(r => r.date <= lastDay)
.reduce((s, r) => s + r.capital, 0);
// Capital projeté pour les échéances futures (après le dernier remb réel et après aujourd'hui)
const lastRembDate = inv.last_remb_date ?? '0000-00-00';
const capitalSimulCumul = (capitalSimulMap[inv.id] ?? [])
.filter(r => r.date > todayStr && r.date > lastRembDate && r.date <= lastDay)
.reduce((s, r) => s + r.capital, 0);
const reinvestsCumul = (reinvestMap[inv.id] ?? [])
.filter(r => r.date <= lastDay)
.reduce((s, r) => s + r.montant, 0);
const capitalRestant = Math.max(0, inv.montant_investi + reinvestsCumul - capitalRembCumul - capitalSimulCumul);
const actif = ['en_cours', 'en_retard', 'procedure'].includes(inv.statut);
if (actif) {
capital += capitalRestant;
if (['en_retard', 'procedure'].includes(inv.statut)) enDefaut += capitalRestant;
} else {
// Remboursé/clôturé : était-il encore actif ce mois-ci ?
// Priorité : date_cible → dernier remboursement réel → exclure
const fin = inv.date_cible ?? inv.last_remb_date ?? null;
if (fin && fin >= firstDay) {
capital += capitalRestant;
}
}
}
capitalMensuel.push({ mois: moisStr, capital: Math.round(capital * 100) / 100, en_defaut: Math.round(enDefaut * 100) / 100 });
}
/* ── Noms des détenteurs par plateforme ─────────────────────── */
const detenteurRows = db.prepare(`
SELECT p.id AS plateforme_id, inv.nom AS detenteur_nom
FROM plateformes p
LEFT JOIN investisseurs inv ON inv.id = p.investisseur_id
WHERE p.user_id = ?
`).all(userId);
const detenteurMap = {};
for (const d of detenteurRows) detenteurMap[d.plateforme_id] = d.detenteur_nom;
/* ── Construire la réponse par plateforme ────────────────────── */
// Indexer les rembs et projections par plateforme_id puis mois
const rembMap = {};
for (const r of remboursesByPlat) {
if (!rembMap[r.plateforme_id]) rembMap[r.plateforme_id] = { id: r.plateforme_id, nom: r.plateforme_nom, detenteur_nom: detenteurMap[r.plateforme_id] ?? null, rembourses: {}, projections: {} };
rembMap[r.plateforme_id].rembourses[r.mois] = { interets_bruts: r.interets_bruts, interets_nets: r.interets_nets, cashback: r.cashback, capital: r.capital };
}
// Ajouter le cashback bonus (parrainage/plateforme) aux remboursements par mois
for (const b of bonusCashbackByPlat) {
if (!rembMap[b.plateforme_id]) rembMap[b.plateforme_id] = { id: b.plateforme_id, nom: b.plateforme_nom, detenteur_nom: detenteurMap[b.plateforme_id] ?? null, rembourses: {}, projections: {} };
const entry = rembMap[b.plateforme_id].rembourses[b.mois];
if (entry) {
entry.cashback = (entry.cashback ?? 0) + b.cashback;
} else {
rembMap[b.plateforme_id].rembourses[b.mois] = { interets_bruts: 0, interets_nets: 0, cashback: b.cashback, capital: 0 };
}
}
for (const p of projectionsByPlat) {
if (!rembMap[p.plateforme_id]) rembMap[p.plateforme_id] = { id: p.plateforme_id, nom: p.plateforme_nom, detenteur_nom: detenteurMap[p.plateforme_id] ?? null, rembourses: {}, projections: {} };
rembMap[p.plateforme_id].projections[p.mois] = { interets_prevus: p.interets_prevus, capital_prevu: p.capital_prevu };
}
const plateformes = Object.values(rembMap).sort((a, b) => a.nom.localeCompare(b.nom));
res.json({ annee, plateformes, capitalMensuel });
});
/**
* GET /api/dashboard/detail-cellule
*
* Retourne le détail ligne par ligne d'une cellule du tableau mensuel :
* - recus : remboursements réels pour plateforme_id + YYYY-MM
* - projetes : simul_remboursements pour plateforme_id + YYYY-MM
* (uniquement les projections sans remboursement réel dans ce mois)
*
* Query params : plateforme_id (int), annee (4 chiffres), mois (01-12), scope=all (opt)
*/
router.get('/detail-cellule', (req, res) => {
const { plateforme_id, annee, mois } = req.query;
const scopeAll = req.query.scope === 'all';
const userId = req.user.id;
if (!annee || !mois)
return res.status(400).json({ error: 'annee et mois sont requis' });
const moisStr = `${annee}-${String(mois).padStart(2, '0')}`;
const platId = plateforme_id ? Number(plateforme_id) : null;
const platFilter = platId ? 'AND i.plateforme_id = ?' : '';
let invWhere, invParams;
if (scopeAll) {
invWhere = 'i.investisseur_id IN (SELECT id FROM investisseurs WHERE user_id = ?)';
invParams = [userId];
} else {
const raw = req.header('X-Investisseur-Id');
const invId = Number(raw);
if (!invId) return res.status(400).json({ error: 'Missing X-Investisseur-Id' });
const row = db.prepare('SELECT id FROM investisseurs WHERE id=? AND user_id=?').get(invId, userId);
if (!row) return res.status(403).json({ error: 'Investisseur not owned by user' });
invWhere = 'i.investisseur_id = ?';
invParams = [invId];
}
const recusParams = platId ? [...invParams, platId, moisStr] : [...invParams, moisStr];
const projetesParams = platId ? [...invParams, platId, moisStr, moisStr] : [...invParams, moisStr, moisStr];
/* ── Remboursements reçus ─────────────────────────────────── */
const recus = db.prepare(`
SELECT
r.id, r.investissement_id, r.date_remb,
r.capital, r.cashback,
r.interets_bruts, r.prelev_sociaux, r.prelev_forfaitaire,
r.interets_nets, r.net_recu,
i.nom_projet, i.plateforme_id,
p.nom AS plateforme_nom,
inv.nom AS detenteur_nom
FROM remboursements r
JOIN investissements i ON i.id = r.investissement_id
JOIN investisseurs inv ON inv.id = i.investisseur_id
LEFT JOIN plateformes p ON p.id = i.plateforme_id
WHERE ${invWhere}
${platFilter}
AND strftime('%Y-%m', r.date_remb) = ?
AND r.type = 'normal'
ORDER BY p.nom, r.date_remb, i.nom_projet
`).all(...recusParams);
/* ── Projections (sans remb réel dans le même mois) ─────── */
const projetes = db.prepare(`
SELECT
s.id, s.investissement_id, s.date_prevue,
s.capital_prevu, s.interets_prevus, s.total_prevu,
s.numero_echeance,
i.nom_projet, i.plateforme_id,
p.nom AS plateforme_nom,
inv.nom AS detenteur_nom
FROM simul_remboursements s
JOIN investissements i ON i.id = s.investissement_id
JOIN investisseurs inv ON inv.id = i.investisseur_id
LEFT JOIN plateformes p ON p.id = i.plateforme_id
WHERE ${invWhere}
${platFilter}
AND strftime('%Y-%m', s.date_prevue) = ?
AND NOT EXISTS (
SELECT 1 FROM remboursements r
WHERE r.investissement_id = s.investissement_id
AND strftime('%Y-%m', r.date_remb) = ?
)
ORDER BY p.nom, s.date_prevue, i.nom_projet
`).all(...projetesParams);
res.json({ recus, projetes });
});
export default router;