Catégorisation des platformes et application de la fiscalité adapté (Impact PSAN , régime micro-BIC)

This commit is contained in:
ocroguennec committed 2026-08-30 16:53:04 +02:00
1 parent 8953d0bb35
commit 33fee587aa
10 files changed
+610 -53

No files matched your search

+180 -3
View File
@@ -117,6 +117,15 @@ router.get('/', (req, res) => {
const interetsNets = recap.interets_bruts - recap.prelev_sociaux - recap.prelev_forfaitaire;
// Au moins une plateforme PSAN (crypto) configurée, quelle que soit sa domiciliation — pour
// exposer cerfa2042cpro (case 5HQ, micro-BNC) au frontend. Même logique que v1/taxreport.js
// (Olivier 30/08/26) : ce handler interne '/' alimente le state `data` de TaxReport.jsx, donc
// il doit porter ce champ lui aussi (l'agrégat /api/v1/taxreport est un endpoint distinct).
const hasPsan = !!db.prepare(
`SELECT 1 FROM plateformes WHERE user_id = ? AND categorie_plateforme = 'psan' LIMIT 1`
).get(req.user.id);
const cerfa2042cpro = hasPsan ? build2042CPro(annee, invCond, invCondBonus, invArg) : null;
const recapTotal = {
interets_bruts: round2(recap.interets_bruts),
prelev_sociaux: round2(recap.prelev_sociaux),
@@ -135,7 +144,7 @@ router.get('/', (req, res) => {
note: "Les cases sont indicatives. Verifiez avec votre situation fiscale reelle.",
};
res.json({ annee, recap: recapTotal, pertes, pertesTotales: round2(pertesTotales), detail, corrections, cases });
res.json({ annee, recap: recapTotal, pertes, pertesTotales: round2(pertesTotales), detail, corrections, cases, has_psan: hasPsan, cerfa2042cpro });
});
router.get('/export', (req, res) => {
@@ -512,8 +521,13 @@ router.get('/years', (req, res) => {
// Filtre commun 2047/2778-SD : seules les plateformes étrangères (domiciliation != FR)
// ET de catégorie "Prestataire de Services de Financement Participatif" (PSFP) sont
// concernées par ces CERFA. Les plateformes "Prestataire de Services sur Actifs
// Numériques" (PSAN) sont exclues même si domiciliées hors de France (demande Olivier
// 30/08/26).
function build2778(annee, invCond, invCondBonus2778, invArg) {
// Plateformes étrangères × investisseur avec au moins un remboursement sur l'année
// Plateformes étrangères PSFP × 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
@@ -525,6 +539,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'normal'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
ORDER BY p.nom, inv.nom
`).all(invArg, annee);
@@ -540,6 +555,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
`).all(invArg, annee);
const platRowsMap = {};
@@ -569,6 +585,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'normal'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, i.investisseur_id, mois
`).all(invArg, annee);
@@ -590,6 +607,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, r.bonus_investisseur_id, mois
`).all(invArg, annee);
@@ -636,6 +654,7 @@ router.get('/2778', (req, res) => {
res.json(build2778(annee, invCond, invCondBonus2778, invArg));
});
// cf. commentaire au-dessus de build2778 : même filtre PSFP.
function build2047(annee, invCond, invCondBonus2047, invArg) {
const rows = db.prepare(`
SELECT
@@ -657,6 +676,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'normal'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, inv.id
`).all(invArg, annee);
@@ -679,6 +699,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, inv.id
`).all(invArg, annee);
@@ -793,6 +814,162 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
É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). ── */
/* ── Filtre commun 2042 C PRO (PSAN) : plateformes de catégorie "Prestataire de Services sur
Actifs Numériques" (crypto), quelle que soit leur domiciliation (FR ou étrangère) — contrairement
à 2047/2778-SD (PSFP étrangères uniquement), la doctrine BNC/micro-BNC pour les intérêts/rewards
crypto (staking) s'applique en fonction de la nature du revenu, pas de la localisation de la
plateforme. Pas de crédit d'impôt conventionnel ici (pas de mécanisme 2047) : les recettes sont
déclarées directement en 2042-C PRO, case 5HQ (régime micro-BNC, abattement forfaitaire de 34 %
appliqué automatiquement par l'administration). Demande Olivier 30/08/26. ── */
function build2042CPro(annee, invCond, invCondBonus2042cpro, invArg) {
// Plateformes PSAN × 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.categorie_plateforme = 'psan'
ORDER BY p.nom, inv.nom
`).all(invArg, annee);
// Intérêts plateforme (pas d'investissement) sur des plateformes PSAN — même logique de
// fusion que pour le 2778/2047 : rattachés à la ligne plateforme × investisseur.
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 ${invCondBonus2042cpro}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.categorie_plateforme = 'psan'
`).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 — même base que 2047/2778 (intérêts bruts
// avant retenue locale) : il n'y a pas de mécanisme de crédit d'impôt pour la partie BNC/PSAN,
// mais la base de calcul de la recette brute reste cohérente avec le reste de l'app.
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.categorie_plateforme = 'psan'
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 ${invCondBonus2042cpro}
AND substr(r.date_remb,1,4) = ?
AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme'
AND p.categorie_plateforme = 'psan'
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 = platRowsMerged.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}`] ?? {},
}));
// Données 2042 C PRO — régime micro-BNC (case 5HQ) : les recettes brutes sont déclarées
// telles quelles, l'administration applique automatiquement l'abattement forfaitaire de 34 %
// pour frais (CGI art. 102 ter). Au-delà du seuil, le régime réel (cases 5KO/5KP) s'applique —
// affiché ici à titre indicatif seulement, l'app ne calcule pas le régime réel (charges réelles
// non suivies dans l'app).
const SEUIL_MICRO_BNC = 77700;
const ABATTEMENT_MICRO_BNC = 0.34;
const totalBrut = round2(plateformes.reduce(
(s, p) => s + Object.values(p.mois).reduce((a, b) => a + b, 0), 0
));
const case_5HQ = Math.round(totalBrut);
const montantImposable = Math.round(case_5HQ * (1 - ABATTEMENT_MICRO_BNC));
return {
annee,
plateformes,
totalBrut,
donnees2042cpro: {
case_5HQ,
abattement_taux: ABATTEMENT_MICRO_BNC,
montant_imposable: montantImposable,
seuil_micro_bnc: SEUIL_MICRO_BNC,
depasse_seuil_micro: case_5HQ > SEUIL_MICRO_BNC,
},
};
}
/* ── 2042 C PRO — Micro-BNC (case 5HQ) — plateformes PSAN (staking / rewards crypto) ──
Contrairement au 2047/2778-SD, ce n'est pas une déclaration de revenus de source étrangère :
les recettes crypto sont des BNC quelle que soit la localisation de la plateforme, reportées
directement en 2042-C PRO sans passer par le 2047 (pas de crédit d'impôt conventionnel). ── */
router.get('/2042cpro', (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 invCondBonus2042cpro = 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(build2042CPro(annee, invCond, invCondBonus2042cpro, invArg));
});
router.get('/2047', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all';
@@ -806,5 +983,5 @@ router.get('/2047', (req, res) => {
res.json(build2047(annee, invCond, invCondBonus2047, invArg));
});
export { buildCerfa2561, build2778, build2047 };
export { buildCerfa2561, build2778, build2047, build2042CPro };
export default router;