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
+600 -43

No files matched your search

+19
View File
@@ -2576,4 +2576,23 @@ if (!smtpCols.includes('documents_quota_count')) {
db.exec('ALTER TABLE smtp_config ADD COLUMN documents_quota_count INTEGER NOT NULL DEFAULT 500'); db.exec('ALTER TABLE smtp_config ADD COLUMN documents_quota_count INTEGER NOT NULL DEFAULT 500');
} }
// ── Migration : catégorie de plateforme (PSFP / PSAN) ──────────────────────
// Distingue les plateformes "Prestataire de Services de Financement
// Participatif" (crowdfunding classique) des "Prestataire de Services sur
// Actifs Numériques" (crypto). Valeur définie au niveau du référentiel admin
// et répliquée sur les plateformes utilisateur à la création ; modifiable
// ensuite par l'utilisateur (Paramètres > Plateformes) comme les autres
// champs hérités (cf. HERITABLE_FIELDS / overridden_fields). Demande Olivier
// 30/08/26.
{
const refColsCatPlat = db.prepare('PRAGMA table_info(plateformes_referentiel)').all().map(c => c.name);
if (!refColsCatPlat.includes('categorie_plateforme')) {
db.exec("ALTER TABLE plateformes_referentiel ADD COLUMN categorie_plateforme TEXT NOT NULL DEFAULT 'psfp'");
}
const platColsCatPlat = db.prepare('PRAGMA table_info(plateformes)').all().map(c => c.name);
if (!platColsCatPlat.includes('categorie_plateforme')) {
db.exec("ALTER TABLE plateformes ADD COLUMN categorie_plateforme TEXT NOT NULL DEFAULT 'psfp'");
}
}
export default db; export default db;
+18 -14
View File
@@ -54,6 +54,7 @@ const Schema = z.object({
fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'), fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'),
taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(), taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(),
type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'), type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'),
categorie_plateforme: z.enum(['psfp', 'psan']).default('psfp'),
methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).default('portefeuille'), methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).default('portefeuille'),
investisseur_id: z.number().int().positive().nullable().optional(), investisseur_id: z.number().int().positive().nullable().optional(),
date_ouverture: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(), date_ouverture: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).nullable().optional(),
@@ -63,7 +64,7 @@ const Schema = z.object({
}); });
// Champs hérités du référentiel (comparés pour calculer overridden_fields) // Champs hérités du référentiel (comparés pour calculer overridden_fields)
const HERITABLE_FIELDS = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut']; const HERITABLE_FIELDS = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'categorie_plateforme', 'logo_filename', 'icone_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
/** /**
* Calcule overridden_fields : champs dont la valeur diffère du référentiel. * Calcule overridden_fields : champs dont la valeur diffère du référentiel.
@@ -222,7 +223,7 @@ function syncCategories(platId, catIds) {
router.get('/referentiel-list', (_req, res) => { router.get('/referentiel-list', (_req, res) => {
const rows = db.prepare(` const rows = db.prepare(`
SELECT pr.id, pr.nom, pr.url, pr.domiciliation, pr.fiscalite, SELECT pr.id, pr.nom, pr.url, pr.domiciliation, pr.fiscalite,
pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.logo_filename, pr.icone_filename, pr.taux_fiscalite_locale, pr.type_produit_fiscal, pr.categorie_plateforme, pr.logo_filename, pr.icone_filename,
pr.methode_remboursement, pr.type_pret_defaut, pr.freq_interets_defaut pr.methode_remboursement, pr.type_pret_defaut, pr.freq_interets_defaut
FROM plateformes_referentiel pr FROM plateformes_referentiel pr
ORDER BY pr.nom ORDER BY pr.nom
@@ -233,7 +234,7 @@ router.get('/referentiel-list', (_req, res) => {
router.get('/', (req, res) => { router.get('/', (req, res) => {
const rows = db.prepare(` const rows = db.prepare(`
SELECT p.id, p.nom, p.url, p.notes, p.domiciliation, p.fiscalite, p.taux_fiscalite_locale, SELECT p.id, p.nom, p.url, p.notes, p.domiciliation, p.fiscalite, p.taux_fiscalite_locale,
p.type_produit_fiscal, p.type_produit_fiscal, p.categorie_plateforme,
p.methode_remboursement, p.investisseur_id, p.date_ouverture, p.logo_filename, p.icone_filename, p.created_at, p.methode_remboursement, p.investisseur_id, p.date_ouverture, p.logo_filename, p.icone_filename, p.created_at,
p.type_pret_defaut, p.freq_interets_defaut, p.type_pret_defaut, p.freq_interets_defaut,
p.referentiel_id, p.overridden_fields, p.referentiel_id, p.overridden_fields,
@@ -283,15 +284,15 @@ router.post('/', (req, res, next) => {
const r = db.prepare(` const r = db.prepare(`
INSERT INTO plateformes INSERT INTO plateformes
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, (user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
methode_remboursement, investisseur_id, date_ouverture, methode_remboursement, investisseur_id, date_ouverture,
type_pret_defaut, freq_interets_defaut, type_pret_defaut, freq_interets_defaut,
logo_filename, icone_filename, logo_filename, icone_filename,
referentiel_id, overridden_fields) referentiel_id, overridden_fields)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`).run( `).run(
req.user.id, body.nom, body.url || null, body.notes || null, req.user.id, body.nom, body.url || null, body.notes || null,
body.domiciliation, fiscalite, taux, typeProduitFiscal, body.domiciliation, fiscalite, taux, typeProduitFiscal, body.categorie_plateforme,
body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null, body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null,
logoFilename, iconeFilename, logoFilename, iconeFilename,
@@ -358,6 +359,7 @@ router.get('/export', (req, res, next) => {
fiscalite: r.fiscalite, fiscalite: r.fiscalite,
taux_fiscalite_locale: r.taux_fiscalite_locale, taux_fiscalite_locale: r.taux_fiscalite_locale,
type_produit_fiscal: r.type_produit_fiscal, type_produit_fiscal: r.type_produit_fiscal,
categorie_plateforme: r.categorie_plateforme,
methode_remboursement: r.methode_remboursement, methode_remboursement: r.methode_remboursement,
date_ouverture: r.date_ouverture, date_ouverture: r.date_ouverture,
type_pret_defaut: r.type_pret_defaut, type_pret_defaut: r.type_pret_defaut,
@@ -426,6 +428,7 @@ router.get('/:id/export', (req, res, next) => {
domiciliation: withAll.domiciliation, fiscalite: withAll.fiscalite, domiciliation: withAll.domiciliation, fiscalite: withAll.fiscalite,
taux_fiscalite_locale: withAll.taux_fiscalite_locale, taux_fiscalite_locale: withAll.taux_fiscalite_locale,
type_produit_fiscal: withAll.type_produit_fiscal, type_produit_fiscal: withAll.type_produit_fiscal,
categorie_plateforme: withAll.categorie_plateforme,
methode_remboursement: withAll.methode_remboursement, methode_remboursement: withAll.methode_remboursement,
date_ouverture: withAll.date_ouverture, date_ouverture: withAll.date_ouverture,
type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut, type_pret_defaut: withAll.type_pret_defaut, freq_interets_defaut: withAll.freq_interets_defaut,
@@ -555,7 +558,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
const fields = [ const fields = [
p.url || null, p.notes || null, p.url || null, p.notes || null,
p.domiciliation || 'france', fiscalite, taux, p.domiciliation || 'france', fiscalite, taux,
p.type_produit_fiscal || '2TT', p.methode_remboursement || 'portefeuille', p.type_produit_fiscal || '2TT', p.categorie_plateforme || 'psfp', p.methode_remboursement || 'portefeuille',
investisseurId, p.date_ouverture || null, investisseurId, p.date_ouverture || null,
p.type_pret_defaut || null, p.freq_interets_defaut || null, p.type_pret_defaut || null, p.freq_interets_defaut || null,
p.logo_filename || null, p.icone_filename || null, p.logo_filename || null, p.icone_filename || null,
@@ -564,7 +567,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
if (existing) { if (existing) {
db.prepare(` db.prepare(`
UPDATE plateformes SET url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, UPDATE plateformes SET url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
type_produit_fiscal=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?, type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
type_pret_defaut=?, freq_interets_defaut=?, type_pret_defaut=?, freq_interets_defaut=?,
logo_filename=?, icone_filename=? logo_filename=?, icone_filename=?
WHERE id=? AND user_id=? WHERE id=? AND user_id=?
@@ -575,10 +578,10 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
const r = db.prepare(` const r = db.prepare(`
INSERT INTO plateformes INSERT INTO plateformes
(user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale, (user_id, nom, url, notes, domiciliation, fiscalite, taux_fiscalite_locale,
type_produit_fiscal, methode_remboursement, investisseur_id, date_ouverture, type_produit_fiscal, categorie_plateforme, methode_remboursement, investisseur_id, date_ouverture,
type_pret_defaut, freq_interets_defaut, type_pret_defaut, freq_interets_defaut,
logo_filename, icone_filename, overridden_fields) logo_filename, icone_filename, overridden_fields)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`).run(req.user.id, p.nom, ...fields, '[]'); `).run(req.user.id, p.nom, ...fields, '[]');
platId = r.lastInsertRowid; platId = r.lastInsertRowid;
created++; created++;
@@ -629,6 +632,7 @@ router.put('/:id', (req, res, next) => {
fiscalite, fiscalite,
taux_fiscalite_locale: taux, taux_fiscalite_locale: taux,
type_produit_fiscal: typeProduitFiscal, type_produit_fiscal: typeProduitFiscal,
categorie_plateforme: body.categorie_plateforme,
logo_filename: current.logo_filename, // logo géré séparément logo_filename: current.logo_filename, // logo géré séparément
methode_remboursement: body.methode_remboursement, methode_remboursement: body.methode_remboursement,
type_pret_defaut: body.type_pret_defaut ?? null, type_pret_defaut: body.type_pret_defaut ?? null,
@@ -640,13 +644,13 @@ router.put('/:id', (req, res, next) => {
const r = db.prepare(` const r = db.prepare(`
UPDATE plateformes UPDATE plateformes
SET nom=?, url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, SET nom=?, url=?, notes=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
type_produit_fiscal=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?, type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, investisseur_id=?, date_ouverture=?,
type_pret_defaut=?, freq_interets_defaut=?, type_pret_defaut=?, freq_interets_defaut=?,
overridden_fields=? overridden_fields=?
WHERE id=? AND user_id=? WHERE id=? AND user_id=?
`).run( `).run(
body.nom, body.url || null, body.notes || null, body.domiciliation, fiscalite, taux, body.nom, body.url || null, body.notes || null, body.domiciliation, fiscalite, taux,
typeProduitFiscal, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null, typeProduitFiscal, body.categorie_plateforme, body.methode_remboursement, body.investisseur_id ?? null, body.date_ouverture || null,
body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null, body.type_pret_defaut ?? null, body.freq_interets_defaut ?? null,
JSON.stringify(overriddenFields), JSON.stringify(overriddenFields),
req.params.id, req.user.id req.params.id, req.user.id
@@ -781,12 +785,12 @@ router.post('/:id/reset', (req, res, next) => {
db.prepare(` db.prepare(`
UPDATE plateformes UPDATE plateformes
SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
type_produit_fiscal=?, logo_filename=?, methode_remboursement=?, type_produit_fiscal=?, categorie_plateforme=?, logo_filename=?, methode_remboursement=?,
type_pret_defaut=?, freq_interets_defaut=?, overridden_fields='[]' type_pret_defaut=?, freq_interets_defaut=?, overridden_fields='[]'
WHERE id=? AND user_id=? WHERE id=? AND user_id=?
`).run( `).run(
ref.nom, ref.url ?? null, ref.domiciliation, ref.fiscalite, ref.taux_fiscalite_locale ?? null, ref.nom, ref.url ?? null, ref.domiciliation, ref.fiscalite, ref.taux_fiscalite_locale ?? null,
ref.type_produit_fiscal, ref.logo_filename ?? null, ref.methode_remboursement || 'portefeuille', ref.type_produit_fiscal, ref.categorie_plateforme || 'psfp', ref.logo_filename ?? null, ref.methode_remboursement || 'portefeuille',
ref.type_pret_defaut ?? null, ref.freq_interets_defaut ?? null, ref.type_pret_defaut ?? null, ref.freq_interets_defaut ?? null,
req.params.id, req.user.id req.params.id, req.user.id
); );
+11 -9
View File
@@ -77,6 +77,7 @@ const Schema = z.object({
fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'), fiscalite: z.enum(['flat_tax', 'sans_fiscalite_locale', 'avec_fiscalite_locale']).default('flat_tax'),
taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(), taux_fiscalite_locale: z.number().min(0).max(100).nullable().optional(),
type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'), type_produit_fiscal: z.enum(['2TT', '2TR']).default('2TT'),
categorie_plateforme: z.enum(['psfp', 'psan']).default('psfp'),
methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).optional().default('portefeuille'), methode_remboursement: z.enum(['portefeuille', 'compte_courant', 'choix_investisseur']).optional().default('portefeuille'),
type_pret_defaut: z.enum(['in_fine', 'amortissable', 'differe']).nullable().optional(), type_pret_defaut: z.enum(['in_fine', 'amortissable', 'differe']).nullable().optional(),
freq_interets_defaut: z.enum(['mensuel', 'trimestriel', 'in_fine']).nullable().optional(), freq_interets_defaut: z.enum(['mensuel', 'trimestriel', 'in_fine']).nullable().optional(),
@@ -321,6 +322,7 @@ router.get('/export', (req, res, next) => {
fiscalite: r.fiscalite, fiscalite: r.fiscalite,
taux_fiscalite_locale: r.taux_fiscalite_locale, taux_fiscalite_locale: r.taux_fiscalite_locale,
type_produit_fiscal: r.type_produit_fiscal, type_produit_fiscal: r.type_produit_fiscal,
categorie_plateforme: r.categorie_plateforme,
description: r.description, description: r.description,
annee_creation: r.annee_creation, annee_creation: r.annee_creation,
investisseurs_types: r.investisseurs_types, investisseurs_types: r.investisseurs_types,
@@ -523,7 +525,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
const fields = [ const fields = [
p.url || null, p.domiciliation || 'france', p.fiscalite || 'flat_tax', p.url || null, p.domiciliation || 'france', p.fiscalite || 'flat_tax',
p.taux_fiscalite_locale ?? null, p.type_produit_fiscal || '2TT', p.taux_fiscalite_locale ?? null, p.type_produit_fiscal || '2TT', p.categorie_plateforme || 'psfp',
p.methode_remboursement || 'portefeuille', p.type_pret_defaut ?? null, p.freq_interets_defaut ?? null, p.methode_remboursement || 'portefeuille', p.type_pret_defaut ?? null, p.freq_interets_defaut ?? null,
p.description ?? null, p.annee_creation ?? null, p.description ?? null, p.annee_creation ?? null,
p.investisseurs_types ?? null, p.investisseurs_types ?? null,
@@ -541,7 +543,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
if (existing) { if (existing) {
db.prepare(` db.prepare(`
UPDATE plateformes_referentiel SET UPDATE plateformes_referentiel SET
url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, type_produit_fiscal=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, type_produit_fiscal=?, categorie_plateforme=?,
methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
description=?, annee_creation=?, investisseurs_types=?, description=?, annee_creation=?, investisseurs_types=?,
regulateur=?, numero_licence=?, is_regule=?, regulateur=?, numero_licence=?, is_regule=?,
@@ -559,7 +561,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
} else { } else {
const r = db.prepare(` const r = db.prepare(`
INSERT INTO plateformes_referentiel INSERT INTO plateformes_referentiel
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, (nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
methode_remboursement, type_pret_defaut, freq_interets_defaut, methode_remboursement, type_pret_defaut, freq_interets_defaut,
description, annee_creation, investisseurs_types, description, annee_creation, investisseurs_types,
regulateur, numero_licence, is_regule, regulateur, numero_licence, is_regule,
@@ -569,7 +571,7 @@ router.post('/import-zip', zipUpload.single('file'), async (req, res, next) => {
garantie_rachat, statistiques_publiques, bonus_inscription, marche_secondaire, investissement_auto, garantie_rachat, statistiques_publiques, bonus_inscription, marche_secondaire, investissement_auto,
url_trustpilot, url_linkedin, url_trustpilot, url_linkedin,
logo_filename, icone_filename, updated_at) logo_filename, icone_filename, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now')) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))
`).run(p.nom, ...fields); `).run(p.nom, ...fields);
refId = r.lastInsertRowid; refId = r.lastInsertRowid;
created++; created++;
@@ -668,7 +670,7 @@ router.post('/', (req, res, next) => {
const r = db.prepare(` const r = db.prepare(`
INSERT INTO plateformes_referentiel INSERT INTO plateformes_referentiel
(nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, (nom, url, domiciliation, fiscalite, taux_fiscalite_locale, type_produit_fiscal, categorie_plateforme,
methode_remboursement, type_pret_defaut, freq_interets_defaut, methode_remboursement, type_pret_defaut, freq_interets_defaut,
logo_filename, description, logo_filename, description,
annee_creation, investisseurs_types, annee_creation, investisseurs_types,
@@ -680,7 +682,7 @@ router.post('/', (req, res, next) => {
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now')) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, datetime('now'))
`).run( `).run(
data.nom, data.url || null, data.domiciliation, data.fiscalite, data.nom, data.url || null, data.domiciliation, data.fiscalite,
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.categorie_plateforme,
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null, data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
data.logo_filename ?? null, data.description ?? null, data.logo_filename ?? null, data.description ?? null,
data.annee_creation ?? null, data.investisseurs_types ?? null, data.annee_creation ?? null, data.investisseurs_types ?? null,
@@ -722,7 +724,7 @@ router.put('/:id', (req, res, next) => {
db.prepare(` db.prepare(`
UPDATE plateformes_referentiel UPDATE plateformes_referentiel
SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?, SET nom=?, url=?, domiciliation=?, fiscalite=?, taux_fiscalite_locale=?,
type_produit_fiscal=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?, type_produit_fiscal=?, categorie_plateforme=?, methode_remboursement=?, type_pret_defaut=?, freq_interets_defaut=?,
logo_filename=?, description=?, logo_filename=?, description=?,
annee_creation=?, investisseurs_types=?, annee_creation=?, investisseurs_types=?,
regulateur=?, numero_licence=?, is_regule=?, regulateur=?, numero_licence=?, is_regule=?,
@@ -735,7 +737,7 @@ router.put('/:id', (req, res, next) => {
WHERE id=? WHERE id=?
`).run( `).run(
data.nom, data.url || null, data.domiciliation, data.fiscalite, data.nom, data.url || null, data.domiciliation, data.fiscalite,
data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.taux_fiscalite_locale ?? null, data.type_produit_fiscal, data.categorie_plateforme,
data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null, data.methode_remboursement || 'portefeuille', data.type_pret_defaut ?? null, data.freq_interets_defaut ?? null,
data.logo_filename ?? null, data.description ?? null, data.logo_filename ?? null, data.description ?? null,
data.annee_creation ?? null, data.investisseurs_types ?? null, data.annee_creation ?? null, data.investisseurs_types ?? null,
@@ -814,7 +816,7 @@ router.post('/:id/push', (req, res, next) => {
const force = req.body?.force === true; const force = req.body?.force === true;
// Champs scalaires poussables // Champs scalaires poussables
const PUSHABLE = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'logo_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut']; const PUSHABLE = ['nom', 'url', 'domiciliation', 'fiscalite', 'taux_fiscalite_locale', 'type_produit_fiscal', 'categorie_plateforme', 'logo_filename', 'methode_remboursement', 'type_pret_defaut', 'freq_interets_defaut'];
// Catégories/secteurs du référentiel // Catégories/secteurs du référentiel
const refCatIds = db.prepare( const refCatIds = db.prepare(
+180 -3
View File
@@ -117,6 +117,15 @@ router.get('/', (req, res) => {
const interetsNets = recap.interets_bruts - recap.prelev_sociaux - recap.prelev_forfaitaire; 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 = { const recapTotal = {
interets_bruts: round2(recap.interets_bruts), interets_bruts: round2(recap.interets_bruts),
prelev_sociaux: round2(recap.prelev_sociaux), 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.", 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) => { 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) { 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(` 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 SELECT DISTINCT p.id, p.nom, inv.id AS investisseur_id, inv.nom AS investisseur_nom, inv.prenom AS investisseur_prenom
FROM remboursements r FROM remboursements r
@@ -525,6 +539,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'normal' AND r.type = 'normal'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
ORDER BY p.nom, inv.nom ORDER BY p.nom, inv.nom
`).all(invArg, annee); `).all(invArg, annee);
@@ -540,6 +555,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme' AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
`).all(invArg, annee); `).all(invArg, annee);
const platRowsMap = {}; const platRowsMap = {};
@@ -569,6 +585,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'normal' AND r.type = 'normal'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, i.investisseur_id, mois GROUP BY p.id, i.investisseur_id, mois
`).all(invArg, annee); `).all(invArg, annee);
@@ -590,6 +607,7 @@ function build2778(annee, invCond, invCondBonus2778, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme' AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, r.bonus_investisseur_id, mois GROUP BY p.id, r.bonus_investisseur_id, mois
`).all(invArg, annee); `).all(invArg, annee);
@@ -636,6 +654,7 @@ router.get('/2778', (req, res) => {
res.json(build2778(annee, invCond, invCondBonus2778, invArg)); res.json(build2778(annee, invCond, invCondBonus2778, invArg));
}); });
// cf. commentaire au-dessus de build2778 : même filtre PSFP.
function build2047(annee, invCond, invCondBonus2047, invArg) { function build2047(annee, invCond, invCondBonus2047, invArg) {
const rows = db.prepare(` const rows = db.prepare(`
SELECT SELECT
@@ -657,6 +676,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'normal' AND r.type = 'normal'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, inv.id GROUP BY p.id, inv.id
`).all(invArg, annee); `).all(invArg, annee);
@@ -679,6 +699,7 @@ function build2047(annee, invCond, invCondBonus2047, invArg) {
AND r.statut IN ('paye','partiel') AND r.statut IN ('paye','partiel')
AND r.type = 'interets_plateforme' AND r.type = 'interets_plateforme'
AND p.domiciliation != 'FR' AND p.domiciliation != 'FR'
AND p.categorie_plateforme = 'psfp'
GROUP BY p.id, inv.id GROUP BY p.id, inv.id
`).all(invArg, annee); `).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" É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 (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). ── */ 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) => { router.get('/2047', (req, res) => {
const annee = req.query.annee || String(new Date().getFullYear() - 1); const annee = req.query.annee || String(new Date().getFullYear() - 1);
const scopeAll = req.query.scope === 'all'; const scopeAll = req.query.scope === 'all';
@@ -806,5 +983,5 @@ router.get('/2047', (req, res) => {
res.json(build2047(annee, invCond, invCondBonus2047, invArg)); res.json(build2047(annee, invCond, invCondBonus2047, invArg));
}); });
export { buildCerfa2561, build2778, build2047 }; export { buildCerfa2561, build2778, build2047, build2042CPro };
export default router; export default router;
+39 -11
View File
@@ -1,6 +1,6 @@
import { Router } from 'express'; import { Router } from 'express';
import db from '../../db/index.js'; import db from '../../db/index.js';
import { buildCerfa2561, build2778, build2047 } from '../taxreport.js'; import { buildCerfa2561, build2778, build2047, build2042CPro } from '../taxreport.js';
const router = Router(); const router = Router();
@@ -21,12 +21,22 @@ const sumBy = (arr, key) => arr.reduce((s, x) => s + (x[key] ?? 0), 0);
* `synthese_2042` reprend les cases 2TT / 2TR / 2BH / 2CK / 2TY telles qu'affichées * `synthese_2042` reprend les cases 2TT / 2TR / 2BH / 2CK / 2TY telles qu'affichées
* en haut de la page Fiscalité. `cerfa2561` détaille le récapitulatif IFU des * en haut de la page Fiscalité. `cerfa2561` détaille le récapitulatif IFU des
* plateformes françaises soumises au PFU. `cerfa2047` (présent seulement si * plateformes françaises soumises au PFU. `cerfa2047` (présent seulement si
* `has_etranger`) détaille, par plateforme étrangère, l'éligibilité au crédit * `has_etranger`) détaille, par plateforme étrangère "Prestataire de Services de
* d'impôt conventionnel (cases 231 à 238 vs 250) et le report vers 8VL/8PL. * Financement Participatif" (PSFP), l'éligibilité au crédit d'impôt conventionnel
* (cases 231 à 238 vs 250) et le report vers 8VL/8PL.
* `cerfa2778_mensuel` (présent seulement si `has_etranger`) donne le détail mensuel * `cerfa2778_mensuel` (présent seulement si `has_etranger`) donne le détail mensuel
* des intérêts bruts par plateforme étrangère, base de calcul du prélèvement * des intérêts bruts par plateforme étrangère PSFP, base de calcul du prélèvement
* forfaitaire obligatoire (PFO) — le PFO n'est légalement dû que si `pfo_assujetti` * forfaitaire obligatoire (PFO) — le PFO n'est légalement dû que si `pfo_assujetti`
* est vrai (seuil de revenu fiscal de référence, cf. Paramétrage → Ma fiscalité). * est vrai (seuil de revenu fiscal de référence, cf. Paramétrage → Ma fiscalité).
* Les plateformes "Prestataire de Services sur Actifs Numériques" (PSAN) ne sont
* jamais concernées par les CERFA 2047/2778-SD, même si domiciliées hors de France.
*
* `cerfa2042cpro` (présent seulement si `has_psan`) donne les recettes brutes crypto
* (staking/rewards) des plateformes PSAN, quelle que soit leur domiciliation — imposables
* en BNC, régime micro par défaut (case 5HQ, abattement forfaitaire de 34 % appliqué
* automatiquement par l'administration ; au-delà de 77 700 €, le régime réel 5KO/5KP
* s'applique, non calculé ici). Ce n'est pas un revenu de source étrangère : aucun crédit
* d'impôt, aucun report vers `synthese_2042`.
* *
* Limite connue : `case_2BH` / `case_2CK` de `synthese_2042` supposent qu'aucune * Limite connue : `case_2BH` / `case_2CK` de `synthese_2042` supposent qu'aucune
* plateforme étrangère n'a été exclue manuellement du calcul PFO dans l'application * plateforme étrangère n'a été exclue manuellement du calcul PFO dans l'application
@@ -67,14 +77,25 @@ router.get('/', (req, res) => {
).get(req.userId); ).get(req.userId);
const pfoAssujetti = prefRow ? prefRow.value === 'true' : false; const pfoAssujetti = prefRow ? prefRow.value === 'true' : false;
// Au moins une plateforme domiciliée hors de France, tous détenteurs du foyer confondus // Au moins une plateforme domiciliée hors de France ET de catégorie "Prestataire de
// et indépendamment de l'année — la table `plateformes` est scopée par `user_id` (pas // Services de Financement Participatif" (PSFP), tous détenteurs du foyer confondus et
// `investisseur_id`, qui n'est que le détenteur assigné) dans toute l'application (cf. // indépendamment de l'année — les plateformes "Prestataire de Services sur Actifs
// GET /api/plateformes), et hasEtranger côté frontend (TaxReport.jsx) est calculé sur // Numériques" (PSAN) ne sont pas concernées par les CERFA 2047/2778-SD (demande Olivier
// cette même liste non filtrée par investisseur actif — un comportement volontairement // 30/08/26), même si elles sont domiciliées hors de France. La table `plateformes` est
// répliqué ici, même avec une clé API mono-investisseur. // scopée par `user_id` (pas `investisseur_id`, qui n'est que le détenteur assigné) dans
// toute l'application (cf. GET /api/plateformes), et hasEtranger côté frontend
// (TaxReport.jsx) est calculé sur cette même liste non filtrée par investisseur actif —
// un comportement volontairement répliqué ici, même avec une clé API mono-investisseur.
const hasEtranger = !!db.prepare( const hasEtranger = !!db.prepare(
`SELECT 1 FROM plateformes WHERE user_id = ? AND domiciliation != 'FR' LIMIT 1` `SELECT 1 FROM plateformes WHERE user_id = ? AND domiciliation != 'FR' AND categorie_plateforme = 'psfp' LIMIT 1`
).get(req.userId);
// Au moins une plateforme "Prestataire de Services sur Actifs Numériques" (PSAN), quelle que
// soit sa domiciliation — les recettes crypto (staking/rewards) sont des BNC de par leur nature,
// pas de par la localisation de la plateforme (contrairement à hasEtranger ci-dessus). Même
// principe de réplication du calcul frontend (TaxReport.jsx) qu'hasEtranger.
const hasPsan = !!db.prepare(
`SELECT 1 FROM plateformes WHERE user_id = ? AND categorie_plateforme = 'psan' LIMIT 1`
).get(req.userId); ).get(req.userId);
const cerfa2561 = buildCerfa2561(annee, invCond, invCondBonus, invParam); const cerfa2561 = buildCerfa2561(annee, invCond, invCondBonus, invParam);
@@ -86,6 +107,11 @@ router.get('/', (req, res) => {
cerfa2778_mensuel = build2778(annee, invCond, invCondBonus, invParam); cerfa2778_mensuel = build2778(annee, invCond, invCondBonus, invParam);
} }
let cerfa2042cpro = null;
if (hasPsan) {
cerfa2042cpro = build2042CPro(annee, invCond, invCondBonus, invParam);
}
// ── Synthèse 2042 — mêmes cases et mêmes formules que le pavé "Cases fiscales 2042 — // ── Synthèse 2042 — mêmes cases et mêmes formules que le pavé "Cases fiscales 2042 —
// synthèse" en haut de la page Fiscalité (TaxReport.jsx, fonction load()) : // synthèse" en haut de la page Fiscalité (TaxReport.jsx, fonction load()) :
// - 2TT : cumul direct des lignes 2561 françaises. 2TY (pertes en capital, // - 2TT : cumul direct des lignes 2561 françaises. 2TY (pertes en capital,
@@ -115,11 +141,13 @@ router.get('/', (req, res) => {
res.json({ res.json({
annee, annee,
has_etranger: hasEtranger, has_etranger: hasEtranger,
has_psan: hasPsan,
pfo_assujetti: pfoAssujetti, pfo_assujetti: pfoAssujetti,
synthese_2042, synthese_2042,
cerfa2561, cerfa2561,
cerfa2047, cerfa2047,
cerfa2778_mensuel, cerfa2778_mensuel,
cerfa2042cpro,
}); });
}); });
@@ -0,0 +1,266 @@
import { useEffect, useState } from 'react';
import { api } from '../api.js';
import { MOIS_LABELS, MOIS_NUMS, r, fmtEUR } from './Cerfa2778Preview.jsx';
// Cases CERFA : montants arrondis à l'euro le plus proche (règle fiscale), sans décimales —
// contrairement au Suivi mensuel (intermédiaire, non déclaré tel quel) qui garde 2 décimales.
const fmt0 = v => {
if (v == null || v === 0) return <span style={{ color: 'var(--text-muted)' }}>- €</span>;
return `${Number(v).toLocaleString('fr-FR', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} €`;
};
function CaseBadge({ code, tone }) {
const success = tone === 'success';
return (
<span style={{
display: 'inline-block', fontFamily: 'monospace', fontWeight: 800, fontSize: 'var(--fs-xs)',
color: success ? 'var(--success)' : 'var(--primary)',
background: success ? 'rgba(16,185,129,0.12)' : 'rgba(99,102,241,0.07)',
borderRadius: 6, padding: '2px 6px', letterSpacing: '.04em', whiteSpace: 'nowrap',
}}>{code}</span>
);
}
/* ── Suivi mensuel — table plateforme × mois, sans les lignes PFO/2778-SD (non applicables : les
recettes PSAN sont des BNC, pas des intérêts soumis au prélèvement forfaitaire obligatoire). ── */
function SuiviMensuelPsan({ annee, plateformes, totauxMois }) {
const cellStyle = {
textAlign: 'right', padding: '6px 10px', fontSize: 'var(--fs-sm)',
whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)',
};
const headStyle = {
textAlign: 'center', padding: '6px 10px', fontSize: 'var(--fs-xs)', fontWeight: 600,
color: 'var(--text-muted)', background: 'var(--surface-2)',
borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap',
};
return (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Suivi mensuel des recettes crypto (BNC)</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Revenus {annee} — plateformes PSAN</span>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 900 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left', minWidth: 180, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Plateforme — Détenteur
</th>
{MOIS_LABELS.map(m => <th key={m} style={headStyle}>{m}</th>)}
<th style={{ ...headStyle, background: 'var(--surface-2)' }}>Total</th>
</tr>
</thead>
<tbody>
{plateformes.map(plat => {
const total = Object.values(plat.mois).reduce((a, b) => a + b, 0);
return (
<tr key={plat.id}>
<td style={{
...cellStyle, textAlign: 'left', position: 'sticky', left: 0,
background: 'var(--surface)', fontWeight: 500,
}}>
{plat.nom}
{(plat.investisseur_nom || plat.investisseur_prenom) && (
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6, fontWeight: 400 }}>
— {(() => { const n = plat.investisseur_nom ?? ''; const p = plat.investisseur_prenom ?? ''; return p && n.startsWith(p) ? n : [p, n].filter(Boolean).join(' '); })()}
</span>
)}
</td>
{MOIS_NUMS.map(m => <td key={m} style={cellStyle}>{fmtEUR(plat.mois[m] ?? null)}</td>)}
<td style={{ ...cellStyle, fontWeight: 600 }}>{fmtEUR(r(total))}</td>
</tr>
);
})}
<tr><td colSpan={14} style={{ height: 4, background: 'var(--surface-2)' }} /></tr>
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700, position: 'sticky', left: 0, background: 'var(--surface-2)' }}>
Total recettes brutes
</td>
{totauxMois.map((t, i) => <td key={i} style={{ ...cellStyle, fontWeight: 700 }}>{fmtEUR(t || null)}</td>)}
<td style={{ ...cellStyle, fontWeight: 800 }}>{fmtEUR(r(totauxMois.reduce((a, b) => a + b, 0)))}</td>
</tr>
</tbody>
</table>
<div style={{ padding: '10px 16px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', borderTop: '1px solid var(--border)' }}>
Base : intérêts/rewards bruts encaissés (valorisés en euros au jour de réception), avant tout abattement.
</div>
</div>
);
}
/* ── Composant principal ── */
export default function Cerfa2042CProPreview({ annee, activeView }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [view, setView] = useState('matrice'); // 'matrice' | 'donnees'
useEffect(() => {
setLoading(true);
setData(null);
const scopeParams = activeView === 'all' ? { scope: 'all' } : {};
api.get('/taxreport/2042cpro', { annee, ...scopeParams })
.then(setData)
.finally(() => setLoading(false));
}, [annee, activeView]);
if (loading || !data) return <div className="card text-muted">Chargement…</div>;
const plateformes = data.plateformes ?? [];
if (plateformes.length === 0) {
return (
<div className="card">
<p className="text-muted" style={{ margin: 0 }}>
Aucune recette crypto (plateforme PSAN) pour {annee}.
</p>
</div>
);
}
const totauxMois = MOIS_NUMS.map(m => r(plateformes.reduce((s, p) => s + (p.mois[m] ?? 0), 0)));
const d = data.donnees2042cpro;
const platLabel = (p) => {
const n = p.investisseur_nom ?? '';
const pr = p.investisseur_prenom ?? '';
const who = pr && n.startsWith(pr) ? n : [pr, n].filter(Boolean).join(' ');
return (
<>
<span style={{ fontWeight: 500 }}>{p.nom}</span>
{who && <span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginLeft: 6 }}>— {who}</span>}
</>
);
};
const cellStyle = {
textAlign: 'right', padding: '7px 10px', fontSize: 'var(--fs-sm)',
whiteSpace: 'nowrap', borderBottom: '1px solid var(--border)',
};
const headStyle = {
textAlign: 'center', padding: '7px 10px', fontSize: 'var(--fs-xs)', fontWeight: 600,
color: 'var(--text-muted)', background: 'var(--surface-2)',
borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap',
};
return (
<div className="card">
<div style={{ display: 'flex', gap: 0, alignItems: 'flex-start' }}>
<nav style={{
width: 200, flexShrink: 0, marginRight: 20,
background: 'var(--surface)', border: '1px solid var(--border)',
borderRadius: 10, padding: '8px 0', boxSizing: 'border-box',
}}>
<div style={{ padding: '6px 12px 8px', fontSize: 'var(--fs-xs)', fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '.06em' }}>
Sections
</div>
<button
className={`account-nav-item${view === 'matrice' ? ' active' : ''}`}
onClick={() => setView('matrice')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M3 3h18v2H3V3zm0 7h12v2H3v-2zm0 7h18v2H3v-2z'/>
</svg>
Suivi mensuel
</button>
<button
className={`account-nav-item${view === 'donnees' ? ' active' : ''}`}
onClick={() => setView('donnees')}
>
<svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' strokeWidth='1.8' strokeLinecap='round' strokeLinejoin='round'>
<path d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'/>
</svg>
Données 2042 C PRO
</button>
<div style={{ margin: '10px 12px 6px', borderTop: '1px solid var(--border)' }} />
<div style={{ padding: '4px 12px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.5 }}>
Plateformes PSAN (crypto)<br/>BNC — régime micro, case 5HQ
</div>
</nav>
<div style={{ flex: 1, minWidth: 0 }}>
{/* ── Vue Suivi mensuel ── */}
{view === 'matrice' && (
<SuiviMensuelPsan annee={annee} plateformes={plateformes} totauxMois={totauxMois} />
)}
{/* ── Vue Données 2042 C PRO ── */}
{view === 'donnees' && (
<div className="card" style={{ padding: 0, overflow: 'auto' }}>
<div style={{ padding: '14px 14px 10px' }}>
<h4 style={{ margin: 0, fontWeight: 700 }}>Bénéfices non commerciaux — Régime micro-BNC</h4>
<span style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)' }}>Vision annuelle {annee} — plateformes PSAN (staking / rewards crypto)</span>
</div>
<div style={{ padding: '0 14px 6px' }}>
<div style={{ fontSize: 'var(--fs-sm)', fontWeight: 600, marginBottom: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="5HQ" /> Recettes brutes par plateforme
</div>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: 700 }}>
<thead>
<tr>
<th style={{ ...headStyle, textAlign: 'left' }}>Plateforme — Détenteur</th>
<th style={headStyle}>Recette brute {annee}</th>
</tr>
</thead>
<tbody>
{plateformes.map(p => {
const total = r(Object.values(p.mois).reduce((a, b) => a + b, 0));
return (
<tr key={p.id}>
<td style={{ ...cellStyle, textAlign: 'left' }}>{platLabel(p)}</td>
<td style={cellStyle}>{fmt0(Math.round(total))}</td>
</tr>
);
})}
<tr style={{ background: 'var(--surface-2)' }}>
<td style={{ ...cellStyle, textAlign: 'left', fontWeight: 700 }}>Total</td>
<td style={{ ...cellStyle, fontWeight: 700 }}>{fmt0(d.case_5HQ)}</td>
</tr>
</tbody>
</table>
<div style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 14px', marginTop: 4, borderTop: '2px solid var(--border)',
background: 'rgba(99,102,241,0.05)',
}}>
<div>
<div style={{ fontWeight: 700, fontSize: 'var(--fs-sm)', display: 'flex', alignItems: 'center', gap: 8 }}>
<CaseBadge code="5HQ" tone="success" /> Micro-BNC — Recettes à reporter en 2042-C PRO
</div>
<div style={{ fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', marginTop: 4, maxWidth: 480, lineHeight: 1.5 }}>
Montant brut, sans déduire l'abattement — celui-ci ({Math.round(d.abattement_taux * 100)} %) est appliqué automatiquement par l'administration.
Bénéfice imposable indicatif : <strong>{fmt0(d.montant_imposable)}</strong>.
</div>
</div>
<div style={{ fontWeight: 800, fontSize: '1.2rem' }}>{fmt0(d.case_5HQ)}</div>
</div>
{d.depasse_seuil_micro && (
<div style={{
margin: '14px', padding: '10px 14px', borderRadius: 8,
background: 'rgba(245,158,11,0.08)', border: '1px solid rgba(245,158,11,0.25)',
fontSize: 'var(--fs-xs)', color: 'var(--text)', lineHeight: 1.6,
}}>
⚠ Recettes supérieures au seuil du régime micro-BNC ({fmt0(d.seuil_micro_bnc)}) : le régime réel s'applique
(cases <strong>5KO</strong> bénéfice / <strong>5KP</strong> déficit), avec déduction des charges réelles
(non calculées par l'application) plutôt que l'abattement forfaitaire de {Math.round(d.abattement_taux * 100)} %.
</div>
)}
<div style={{ padding: '10px 14px 14px', fontSize: 'var(--fs-xs)', color: 'var(--text-muted)', lineHeight: 1.6 }}>
Les intérêts/rewards perçus via des plateformes de type « Prestataire de Services sur Actifs Numériques »
(staking, récompenses crypto) sont imposés en bénéfices non commerciaux (BNC), quelle que soit la
localisation de la plateforme — il ne s'agit pas d'un revenu de source étrangère : contrairement au 2047/2778-SD,
ce montant n'ouvre droit à aucun crédit d'impôt et ne se déclare pas via l'imprimé n° 2047. Il se reporte
directement sur la déclaration n° 2042-C PRO, rubrique « Régime micro BNC ».
</div>
</div>
)}
</div>
</div>
</div>
);
}
+11 -1
View File
@@ -44,7 +44,8 @@ function IconUpload() {
/* ── Constantes ───────────────────────────────────────────────── */ /* ── Constantes ───────────────────────────────────────────────── */
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—'; const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const FISCALITE_LABELS = { flat_tax: 'Flat tax (30%)', sans_fiscalite_locale: 'Sans fiscalité locale', avec_fiscalite_locale: 'Avec fiscalité locale' }; const FISCALITE_LABELS = { flat_tax: 'Flat tax (30%)', sans_fiscalite_locale: 'Sans fiscalité locale', avec_fiscalite_locale: 'Avec fiscalité locale' };
const EMPTY_REF = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', methode_remboursement: 'portefeuille', type_pret_defaut: '', freq_interets_defaut: '', description: '', categories: [], categories_inv_ids: [], secteurs_inv_ids: [] }; const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
const EMPTY_REF = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', categorie_plateforme: 'psfp', methode_remboursement: 'portefeuille', type_pret_defaut: '', freq_interets_defaut: '', description: '', categories: [], categories_inv_ids: [], secteurs_inv_ids: [] };
/* ── Modal création / édition référentiel ─────────────────────── */ /* ── Modal création / édition référentiel ─────────────────────── */
function RefModal({ row, onClose, onSaved }) { function RefModal({ row, onClose, onSaved }) {
@@ -59,6 +60,7 @@ function RefModal({ row, onClose, onSaved }) {
fiscalite: row.fiscalite || 'flat_tax', fiscalite: row.fiscalite || 'flat_tax',
taux_fiscalite_locale: row.taux_fiscalite_locale != null ? String(row.taux_fiscalite_locale) : '', taux_fiscalite_locale: row.taux_fiscalite_locale != null ? String(row.taux_fiscalite_locale) : '',
type_produit_fiscal: row.type_produit_fiscal || '2TT', type_produit_fiscal: row.type_produit_fiscal || '2TT',
categorie_plateforme: row.categorie_plateforme || 'psfp',
methode_remboursement: row.methode_remboursement || 'portefeuille', methode_remboursement: row.methode_remboursement || 'portefeuille',
type_pret_defaut: row.type_pret_defaut || '', type_pret_defaut: row.type_pret_defaut || '',
freq_interets_defaut: row.freq_interets_defaut || '', freq_interets_defaut: row.freq_interets_defaut || '',
@@ -93,6 +95,7 @@ function RefModal({ row, onClose, onSaved }) {
taux_fiscalite_locale: fiscalite === 'avec_fiscalite_locale' && form.taux_fiscalite_locale !== '' taux_fiscalite_locale: fiscalite === 'avec_fiscalite_locale' && form.taux_fiscalite_locale !== ''
? Number(form.taux_fiscalite_locale) : null, ? Number(form.taux_fiscalite_locale) : null,
type_produit_fiscal: form.type_produit_fiscal, type_produit_fiscal: form.type_produit_fiscal,
categorie_plateforme: form.categorie_plateforme || 'psfp',
methode_remboursement: form.methode_remboursement || 'portefeuille', methode_remboursement: form.methode_remboursement || 'portefeuille',
type_pret_defaut: form.type_pret_defaut || null, type_pret_defaut: form.type_pret_defaut || null,
freq_interets_defaut: form.freq_interets_defaut || null, freq_interets_defaut: form.freq_interets_defaut || null,
@@ -181,6 +184,13 @@ function RefModal({ row, onClose, onSaved }) {
<option value="2TR">2TR — Revenus assimilés</option> <option value="2TR">2TR — Revenus assimilés</option>
</select> </select>
</div> </div>
<div>
<label>Catégorie de plateforme</label>
<select value={form.categorie_plateforme || 'psfp'} onChange={e => set('categorie_plateforme', e.target.value)}>
<option value="psfp">{CATEGORIE_PLATEFORME_LABELS.psfp}</option>
<option value="psan">{CATEGORIE_PLATEFORME_LABELS.psan}</option>
</select>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div> <div>
<label>Méthode de remboursement</label> <label>Méthode de remboursement</label>
+6
View File
@@ -35,6 +35,7 @@ const INV_TYPES = {
les_deux: 'Particulier & Entreprise', les_deux: 'Particulier & Entreprise',
}; };
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—'; const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
// ── Composants helpers ──────────────────────────────────────────────────────── // ── Composants helpers ────────────────────────────────────────────────────────
@@ -174,6 +175,7 @@ export default function PlatformeProfile() {
fiscalite: form.fiscalite, fiscalite: form.fiscalite,
taux_fiscalite_locale: form.taux_fiscalite_locale !== '' && form.taux_fiscalite_locale != null ? Number(form.taux_fiscalite_locale) : null, taux_fiscalite_locale: form.taux_fiscalite_locale !== '' && form.taux_fiscalite_locale != null ? Number(form.taux_fiscalite_locale) : null,
type_produit_fiscal: form.type_produit_fiscal || '2TT', type_produit_fiscal: form.type_produit_fiscal || '2TT',
categorie_plateforme: form.categorie_plateforme || 'psfp',
logo_filename: data.logo_filename ?? null, logo_filename: data.logo_filename ?? null,
description: form.description || null, description: form.description || null,
categories: data.categories || [], categories: data.categories || [],
@@ -275,6 +277,9 @@ export default function PlatformeProfile() {
{countryLabel(data.domiciliation)} {countryLabel(data.domiciliation)}
</span> </span>
)} )}
<span title="Modifiable depuis la fiche du référentiel (liste Plateformes)" style={{ padding: '3px 10px', borderRadius: 20, background: 'var(--surface-2)', color: 'var(--text-muted)', fontSize: 12, flexShrink: 0 }}>
{CATEGORIE_PLATEFORME_LABELS[data.categorie_plateforme] ?? CATEGORIE_PLATEFORME_LABELS.psfp}
</span>
</div> </div>
{editing {editing
? <textarea value={form.description ?? ''} onChange={setInput('description')} ? <textarea value={form.description ?? ''} onChange={setInput('description')}
@@ -782,6 +787,7 @@ function ProfilImportBlock({ data, id, onApplied, catsInv = [], secteursInv = []
fiscalite: data.fiscalite, fiscalite: data.fiscalite,
taux_fiscalite_locale: data.taux_fiscalite_locale ?? null, taux_fiscalite_locale: data.taux_fiscalite_locale ?? null,
type_produit_fiscal: data.type_produit_fiscal || '2TT', type_produit_fiscal: data.type_produit_fiscal || '2TT',
categorie_plateforme: data.categorie_plateforme || 'psfp',
logo_filename: data.logo_filename ?? null, logo_filename: data.logo_filename ?? null,
description: data.description || null, description: data.description || null,
categories: data.categories || [], categories: data.categories || [],
+35 -4
View File
@@ -13,6 +13,7 @@ import CerfaRecapTable from '../components/CerfaRecapTable.jsx';
import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx'; import Cerfa2778Preview from '../components/Cerfa2778Preview.jsx';
import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx'; import Cerfa2047Preview from '../components/Cerfa2047Preview.jsx';
import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx'; import Cerfa2042Preview from '../components/Cerfa2042Preview.jsx';
import Cerfa2042CProPreview from '../components/Cerfa2042CProPreview.jsx';
/* ── YearSelector ────────────────────────────────────────────── */ /* ── YearSelector ────────────────────────────────────────────── */
function YearSelector({ annee, setAnnee, availableYears }) { function YearSelector({ annee, setAnnee, availableYears }) {
@@ -229,13 +230,15 @@ export default function TaxReport() {
const [detailExpanded, setDetailExpanded] = useState(false); const [detailExpanded, setDetailExpanded] = useState(false);
const [cerfaExpanded, setCerfaExpanded] = useState(false); const [cerfaExpanded, setCerfaExpanded] = useState(false);
const [hasPlateformes, setHasPlateformes] = useState(true); // true par défaut pour éviter un flash du mauvais message const [hasPlateformes, setHasPlateformes] = useState(true); // true par défaut pour éviter un flash du mauvais message
const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD) const [hasEtranger, setHasEtranger] = useState(false); // au moins une plateforme étrangère PSFP configurée (pour l'onglet CERFA 2047 — indépendant de l'année sélectionnée, même principe que le 2778-SD ; les plateformes PSAN ne sont pas concernées)
const [hasPsan, setHasPsan] = useState(false); // au moins une plateforme PSAN (crypto) configurée, quelle que soit sa domiciliation (pour l'onglet CERFA 2042 C PRO — indépendant de l'année sélectionnée, même principe que hasEtranger)
/* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */ /* ── Plateformes configurées ? (pour distinguer "pas de plateforme" de "pas encore de données") ── */
useEffect(() => { useEffect(() => {
api.get('/plateformes').then(p => { api.get('/plateformes').then(p => {
setHasPlateformes((p || []).length > 0); setHasPlateformes((p || []).length > 0);
setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR')); setHasEtranger((p || []).some(pl => pl.domiciliation && pl.domiciliation !== 'FR' && (pl.categorie_plateforme ?? 'psfp') === 'psfp'));
setHasPsan((p || []).some(pl => (pl.categorie_plateforme ?? 'psfp') === 'psan'));
}).catch(() => {}); }).catch(() => {});
}, []); }, []);
@@ -366,7 +369,9 @@ export default function TaxReport() {
<div className="card text-muted">Chargement…</div> <div className="card text-muted">Chargement…</div>
) : ( ) : (
<> <>
{!detailExpanded && !cerfaExpanded && data2042 && <div className="card"> {!detailExpanded && !cerfaExpanded && data2042 && (
<div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'stretch' }}>
<div className="card" style={{ flex: '3 1 420px' }}>
<h3 style={{ marginTop: 0 }}>Cases fiscales 2042 — synthèse {annee}</h3> <h3 style={{ marginTop: 0 }}>Cases fiscales 2042 — synthèse {annee}</h3>
<div className="kpi-grid"> <div className="kpi-grid">
{data2042.case_2TT > 0 && <div className="kpi"><div className="label">Case 2TT — Prêts participatifs (FR)</div><div className="value">{fmtEUR(data2042.case_2TT)}</div></div>} {data2042.case_2TT > 0 && <div className="kpi"><div className="label">Case 2TT — Prêts participatifs (FR)</div><div className="value">{fmtEUR(data2042.case_2TT)}</div></div>}
@@ -378,7 +383,21 @@ export default function TaxReport() {
<p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}> <p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}>
⚠ Cases indicatives combinant plateformes françaises (IFU automatique) et étrangères. Référez-vous à la notice 2041-GFI. ⚠ Cases indicatives combinant plateformes françaises (IFU automatique) et étrangères. Référez-vous à la notice 2041-GFI.
</p> </p>
</div>} </div>
{hasPsan && data?.cerfa2042cpro?.donnees2042cpro && (
<div className="card" style={{ flex: '1 1 220px' }}>
<h3 style={{ marginTop: 0 }}>Cases fiscales 2042 C PRO — synthèse {annee}</h3>
<div className="kpi-grid">
<div className="kpi"><div className="label">Case 5HQ — Micro-BNC (PSAN)</div><div className="value">{fmtEUR(data.cerfa2042cpro.donnees2042cpro.case_5HQ)}</div></div>
</div>
<p className="text-muted" style={{ marginTop: 12, fontSize: 12 }}>
⚠ Montant brut ; l'abattement de 34 % (régime micro-BNC) est appliqué automatiquement par l'administration.
</p>
</div>
)}
</div>
)}
{!detailExpanded && !cerfaExpanded && data2042 && ( {!detailExpanded && !cerfaExpanded && data2042 && (
<div className="card" style={{ marginTop: 16 }}> <div className="card" style={{ marginTop: 16 }}>
@@ -409,6 +428,13 @@ export default function TaxReport() {
texte="Les intérêts perçus via des plateformes étrangères sont soumis à un prélèvement forfaitaire obligatoire (acompte) si vous avez un revenu fiscal de référence de l'avant-dernière année de 25 000 € pour un personne seule ou 50 000 € pour couple marié ou pacsé. Malgré la déclaration mensuelle aux autorités fiscales, la déclaration annuelle au travers du CERFA 2047 reste obligatoire." texte="Les intérêts perçus via des plateformes étrangères sont soumis à un prélèvement forfaitaire obligatoire (acompte) si vous avez un revenu fiscal de référence de l'avant-dernière année de 25 000 € pour un personne seule ou 50 000 € pour couple marié ou pacsé. Malgré la déclaration mensuelle aux autorités fiscales, la déclaration annuelle au travers du CERFA 2047 reste obligatoire."
/> />
)} )}
{hasPsan && (
<InfoLigneCerfa
code="2042-C PRO"
titre="Intérêts d'emprunts sur une plateforme d'actif numérique (Crowdlending sur RWA)"
texte="Les intérêts perçus via des plateformes de type « Prestataire de Services sur Actifs Numériques » pour des prêts de type Crowdlending sur des RWA (Real World Asset) sont des bénéfices non commerciaux (BNC), quelle que soit leur localisation. Pas de déclaration CERFA 2047, ni de crédit d'impôt possible. Une déclaration directe en 2042-C PRO en régime micro-BNC (case 5HQ)."
/>
)}
</div> </div>
<div style={{ flex: '1 1 320px', minWidth: 280, display: 'flex', justifyContent: 'center' }}> <div style={{ flex: '1 1 320px', minWidth: 280, display: 'flex', justifyContent: 'center' }}>
<CerfaFlowDiagram hasEtranger={hasEtranger} /> <CerfaFlowDiagram hasEtranger={hasEtranger} />
@@ -445,6 +471,7 @@ export default function TaxReport() {
{!detailExpanded && !cerfaExpanded && ( {!detailExpanded && !cerfaExpanded && (
<div className="dr-tabs"> <div className="dr-tabs">
<button className={`dr-tab${activeTab === '2042' ? ' active' : ''}`} onClick={() => setActiveTab('2042')}>CERFA 2042</button> <button className={`dr-tab${activeTab === '2042' ? ' active' : ''}`} onClick={() => setActiveTab('2042')}>CERFA 2042</button>
{hasPsan && <button className={`dr-tab${activeTab === '2042cpro' ? ' active' : ''}`} onClick={() => setActiveTab('2042cpro')}>CERFA 2042 C PRO</button>}
<button className={`dr-tab${activeTab === 'cerfa' ? ' active' : ''}`} onClick={() => setActiveTab('cerfa')}>CERFA 2561 (IFU)</button> <button className={`dr-tab${activeTab === 'cerfa' ? ' active' : ''}`} onClick={() => setActiveTab('cerfa')}>CERFA 2561 (IFU)</button>
{hasEtranger && <button className={`dr-tab${activeTab === '2047' ? ' active' : ''}`} onClick={() => setActiveTab('2047')}>CERFA 2047</button>} {hasEtranger && <button className={`dr-tab${activeTab === '2047' ? ' active' : ''}`} onClick={() => setActiveTab('2047')}>CERFA 2047</button>}
{pfoAssujetti && <button className={`dr-tab${activeTab === '2778' ? ' active' : ''}`} onClick={() => setActiveTab('2778')}>CERFA 2778-SD</button>} {pfoAssujetti && <button className={`dr-tab${activeTab === '2778' ? ' active' : ''}`} onClick={() => setActiveTab('2778')}>CERFA 2778-SD</button>}
@@ -473,6 +500,10 @@ export default function TaxReport() {
{activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && ( {activeTab === '2778' && pfoAssujetti && !detailExpanded && !cerfaExpanded && (
<Cerfa2778Preview annee={annee} activeView={activeView} /> <Cerfa2778Preview annee={annee} activeView={activeView} />
)} )}
{activeTab === '2042cpro' && hasPsan && !detailExpanded && !cerfaExpanded && (
<Cerfa2042CProPreview annee={annee} activeView={activeView} />
)}
</> </>
); );
} }
@@ -24,6 +24,7 @@ function localToday() {
} }
const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—'; const countryLabel = code => COUNTRIES.find(c => c.code === code)?.name ?? code ?? '—';
const CATEGORIE_PLATEFORME_LABELS = { psfp: 'Prestataire de Services de Financement Participatif', psan: 'Prestataire de Services sur Actifs Numériques' };
const FISCALITE_LABELS = { const FISCALITE_LABELS = {
flat_tax: 'Flat Tax', flat_tax: 'Flat Tax',
sans_fiscalite_locale: 'Sans fiscalité locale', sans_fiscalite_locale: 'Sans fiscalité locale',
@@ -51,7 +52,7 @@ function fmtFiscalite(p) {
return base; return base;
} }
const EMPTY_PLAT = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', methode_remboursement: 'portefeuille', investisseur_id: null, date_ouverture: '', logo_filename: null, type_pret_defaut: '', freq_interets_defaut: '', referentiel_id: null }; const EMPTY_PLAT = { nom: '', url: '', domiciliation: 'france', fiscalite: 'flat_tax', taux_fiscalite_locale: '', type_produit_fiscal: '2TT', categorie_plateforme: 'psfp', methode_remboursement: 'portefeuille', investisseur_id: null, date_ouverture: '', logo_filename: null, type_pret_defaut: '', freq_interets_defaut: '', referentiel_id: null };
const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/'; const LOGO_BASE = (import.meta.env.VITE_API_URL || '/api').replace(/\/api$/, '') + '/api/logos/';
const logoUrl = (filename) => filename ? LOGO_BASE + filename : null; const logoUrl = (filename) => filename ? LOGO_BASE + filename : null;
@@ -211,6 +212,7 @@ function PlatDetailPanel({ plat, onEdit }) {
plat.date_ouverture && { label: "Date d'ouverture", value: fmtDate(plat.date_ouverture) }, plat.date_ouverture && { label: "Date d'ouverture", value: fmtDate(plat.date_ouverture) },
{ label: 'Domiciliation', value: plat.domiciliation ? <span style={{ display:'inline-flex', alignItems:'center', gap:5 }}><FlagIcon code={plat.domiciliation} size={16} />{countryLabel(plat.domiciliation)}</span> : '—' }, { label: 'Domiciliation', value: plat.domiciliation ? <span style={{ display:'inline-flex', alignItems:'center', gap:5 }}><FlagIcon code={plat.domiciliation} size={16} />{countryLabel(plat.domiciliation)}</span> : '—' },
{ label: 'Fiscalité', value: fmtFiscalite(plat) }, { label: 'Fiscalité', value: fmtFiscalite(plat) },
{ label: 'Catégorie de plateforme', value: CATEGORIE_PLATEFORME_LABELS[plat.categorie_plateforme] ?? CATEGORIE_PLATEFORME_LABELS.psfp },
plat.domiciliation === 'FR' && { plat.domiciliation === 'FR' && {
label: 'Déclaration 2561', label: 'Déclaration 2561',
value: (plat.type_produit_fiscal ?? '2TT') === '2TR' value: (plat.type_produit_fiscal ?? '2TT') === '2TR'
@@ -581,6 +583,15 @@ function PlatForm({ state, setter, logoFile, setLogoFile, logoPreview, setLogoPr
</select> </select>
))} ))}
{/* Catégorie de plateforme */}
{row('Catégorie de plateforme', (
<select value={state.categorie_plateforme || 'psfp'} onChange={e => setter(s => ({ ...s, categorie_plateforme: e.target.value }))}
style={{ width: '100%' }}>
<option value="psfp">{CATEGORIE_PLATEFORME_LABELS.psfp}</option>
<option value="psan">{CATEGORIE_PLATEFORME_LABELS.psan}</option>
</select>
))}
{/* Méthode de remboursement */} {/* Méthode de remboursement */}
{row('Versement des remboursements', ( {row('Versement des remboursements', (
<select value={state.methode_remboursement || 'portefeuille'} onChange={e => setter(s => ({ ...s, methode_remboursement: e.target.value }))} <select value={state.methode_remboursement || 'portefeuille'} onChange={e => setter(s => ({ ...s, methode_remboursement: e.target.value }))}
@@ -848,6 +859,7 @@ export default function PlateformesSection() {
fiscalite: ref.fiscalite || 'flat_tax', fiscalite: ref.fiscalite || 'flat_tax',
taux_fiscalite_locale: ref.taux_fiscalite_locale ?? null, taux_fiscalite_locale: ref.taux_fiscalite_locale ?? null,
type_produit_fiscal: ref.type_produit_fiscal || '2TT', type_produit_fiscal: ref.type_produit_fiscal || '2TT',
categorie_plateforme: ref.categorie_plateforme || 'psfp',
methode_remboursement: ref.methode_remboursement || 'portefeuille', methode_remboursement: ref.methode_remboursement || 'portefeuille',
type_pret_defaut: ref.type_pret_defaut || null, type_pret_defaut: ref.type_pret_defaut || null,
freq_interets_defaut: ref.freq_interets_defaut || null, freq_interets_defaut: ref.freq_interets_defaut || null,
@@ -952,6 +964,7 @@ export default function PlateformesSection() {
fiscalite: p.fiscalite || 'flat_tax', fiscalite: p.fiscalite || 'flat_tax',
taux_fiscalite_locale: p.taux_fiscalite_locale ?? '', taux_fiscalite_locale: p.taux_fiscalite_locale ?? '',
type_produit_fiscal: p.type_produit_fiscal || '2TT', type_produit_fiscal: p.type_produit_fiscal || '2TT',
categorie_plateforme: p.categorie_plateforme || 'psfp',
methode_remboursement: p.methode_remboursement || 'portefeuille', methode_remboursement: p.methode_remboursement || 'portefeuille',
investisseur_id: p.investisseur_id ?? null, investisseur_id: p.investisseur_id ?? null,
date_ouverture: p.date_ouverture || '', date_ouverture: p.date_ouverture || '',
@@ -974,6 +987,7 @@ export default function PlateformesSection() {
taux_fiscalite_locale: editPlat.fiscalite === 'avec_fiscalite_locale' && editPlat.taux_fiscalite_locale !== '' taux_fiscalite_locale: editPlat.fiscalite === 'avec_fiscalite_locale' && editPlat.taux_fiscalite_locale !== ''
? Number(editPlat.taux_fiscalite_locale) : null, ? Number(editPlat.taux_fiscalite_locale) : null,
type_produit_fiscal: editPlat.type_produit_fiscal || '2TT', type_produit_fiscal: editPlat.type_produit_fiscal || '2TT',
categorie_plateforme: editPlat.categorie_plateforme || 'psfp',
methode_remboursement: editPlat.methode_remboursement || 'portefeuille', methode_remboursement: editPlat.methode_remboursement || 'portefeuille',
investisseur_id: editPlat.investisseur_id ? Number(editPlat.investisseur_id) : null, investisseur_id: editPlat.investisseur_id ? Number(editPlat.investisseur_id) : null,
date_ouverture: editPlat.date_ouverture || null, date_ouverture: editPlat.date_ouverture || null,