maj
This commit is contained in:
1 parent
1e3994f17f
commit
624d32ff7c
17 files changed
+2192
-1
No files matched your search
@@ -0,0 +1,18 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
anchor = """ { nom: 'Wallet crypto (conservation par un tiers)', categories: ['Actifs numériques'], description: "Cryptomonnaies détenues dans un wallet dont les clés sont conservées par un tiers de confiance (service de custody dédié, family office...), par opposition à l'auto-conservation ou à un compte sur une plateforme d'échange.", plafond_eur: null, regime_fiscal: "Flat tax (PFU) sur les plus-values de cession (particuliers), déclaration 2086", supports: [] },
|
||||
];"""
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
new_item = """ { nom: 'Wallet crypto (conservation par un tiers)', categories: ['Actifs numériques'], description: "Cryptomonnaies détenues dans un wallet dont les clés sont conservées par un tiers de confiance (service de custody dédié, family office...), par opposition à l'auto-conservation ou à un compte sur une plateforme d'échange.", plafond_eur: null, regime_fiscal: "Flat tax (PFU) sur les plus-values de cession (particuliers), déclaration 2086", supports: [] },
|
||||
{ nom: 'Crowdlending (prêts aux entreprises/PME)', categories: ['Financement participatif / private equity'], description: "Prêt participatif à des entreprises/PME hors immobilier (le coeur de métier de cette application) — distinct de « Crowdfunding immobilier » (prêts adossés à une opération immobilière) et de « Crowdequity » (prise de participation au capital, pas un prêt).", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les intérêts (flat tax / PFU, ou option barème)", supports: [] },
|
||||
];"""
|
||||
|
||||
content = content.replace(anchor, new_item, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Crowdlending enveloppe inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,36 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1) Nouvelle catégorie "Investissement forestier & viticole" (GFF + GFV) ajoutée à la liste des
|
||||
# catégories toujours insérées (INSERT OR IGNORE).
|
||||
anchor_cat = " for (const nom of ['Immobilier physique', 'Actifs numériques', 'Métaux précieux', 'Objets de collection']) {"
|
||||
assert content.count(anchor_cat) == 1, f"anchor_cat count={content.count(anchor_cat)}"
|
||||
new_cat = " for (const nom of ['Immobilier physique', 'Actifs numériques', 'Métaux précieux', 'Objets de collection', 'Investissement forestier & viticole']) {"
|
||||
content = content.replace(anchor_cat, new_cat, 1)
|
||||
|
||||
# 2) 7 nouvelles enveloppes ajoutées à EXTRA_ENVELOPPES (demande Olivier du 27/09/26, à partir
|
||||
# d'une capture d'écran d'un site de conseil en gestion de patrimoine listant des placements
|
||||
# absents du référentiel) : Dette privée, capital-investissement réservé (FPCI/SLP/FPS),
|
||||
# Infrastructures non cotées, Groupement Foncier Forestier, Groupement Foncier Viticole,
|
||||
# LMNP/LMP (distinct de l'immobilier locatif nu déjà présent), SCPI en démembrement.
|
||||
anchor_last_item = """ { nom: 'Crowdlending (prêts aux entreprises/PME)', categories: ['Financement participatif / private equity'], description: "Prêt participatif à des entreprises/PME hors immobilier (le coeur de métier de cette application) — distinct de « Crowdfunding immobilier » (prêts adossés à une opération immobilière) et de « Crowdequity » (prise de participation au capital, pas un prêt).", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les intérêts (flat tax / PFU, ou option barème)", supports: [] },
|
||||
];"""
|
||||
assert content.count(anchor_last_item) == 1, f"anchor_last_item count={content.count(anchor_last_item)}"
|
||||
|
||||
new_items = """ { nom: 'Crowdlending (prêts aux entreprises/PME)', categories: ['Financement participatif / private equity'], description: "Prêt participatif à des entreprises/PME hors immobilier (le coeur de métier de cette application) — distinct de « Crowdfunding immobilier » (prêts adossés à une opération immobilière) et de « Crowdequity » (prise de participation au capital, pas un prêt).", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les intérêts (flat tax / PFU, ou option barème)", supports: [] },
|
||||
{ nom: 'Dette privée (fonds)', categories: ['Financement participatif / private equity'], description: "Fonds de dette privée (private debt) — prêts directs à des entreprises via un véhicule de fonds professionnel, hors plateforme de crowdlending grand public.", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les revenus/plus-values de cession de parts (flat tax / PFU, ou option barème)", supports: [] },
|
||||
{ nom: 'Capital-investissement réservé (FPCI / SLP / FPS)', categories: ['Financement participatif / private equity'], description: "Fonds de capital-investissement réservés aux investisseurs avertis/professionnels — Fonds Professionnel de Capital Investissement (FPCI), Société de Libre Partenariat (SLP), Fonds Professionnel Spécialisé (FPS) ; distinct du FCPR grand public (support disponible en assurance-vie/PER/contrat de capitalisation) et de la participation directe non cotée (prise de participation en direct, hors fonds).", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les plus-values de cession de parts (flat tax / PFU, ou option barème) ; exonération possible sous conditions de durée de détention, comme pour un FCPR", supports: [] },
|
||||
{ nom: 'Infrastructures non cotées', categories: ['Financement participatif / private equity'], description: "Fonds d'investissement dans des actifs d'infrastructure non cotés (énergie, transport, télécoms...).", plafond_eur: null, regime_fiscal: "Fiscalité classique sur les revenus/plus-values de cession de parts (flat tax / PFU, ou option barème)", supports: [] },
|
||||
{ nom: 'Groupement Foncier Forestier (GFF)', categories: ['Investissement forestier & viticole'], description: "Investissement forestier via un groupement foncier forestier — parts donnant une quote-part de bois et forêts en gestion collective.", plafond_eur: null, regime_fiscal: "Réduction d'IR à l'entrée possible sous conditions (dispositif DEFI-Forêt), exonération partielle d'IFI, transmission facilitée sous conditions d'engagement de gestion durable", supports: [] },
|
||||
{ nom: 'Groupement Foncier Viticole (GFV)', categories: ['Investissement forestier & viticole'], description: "Investissement viticole via un groupement foncier viticole — parts donnant une quote-part de vignes louées à un exploitant via un bail rural.", plafond_eur: null, regime_fiscal: "Revenus fonciers (loyers du bail rural), exonération partielle d'IFI et de droits de succession/donation sous conditions", supports: [] },
|
||||
{ nom: 'Immobilier locatif meublé (LMNP/LMP)', categories: ['Immobilier physique'], description: "Location meublée en direct — statut Loueur en Meublé Non Professionnel (LMNP) ou Professionnel (LMP) selon les seuils de recettes et la composition du foyer fiscal ; distinct de « Immobilier locatif (détention directe) » qui suppose une location nue (revenus fonciers).", plafond_eur: null, regime_fiscal: "Revenus imposés en BIC (micro-BIC ou régime réel avec amortissement du bien) ; le statut LMP ouvre en plus à l'imputation des déficits sur le revenu global et à un régime de plus-value professionnelle", supports: [] },
|
||||
{ nom: 'SCPI en démembrement (nue-propriété / usufruit temporaire)', categories: ['Immobilier papier'], description: "SCPI achetée en démembrement de propriété (nue-propriété ou usufruit temporaire), typiquement sur 5 à 10 ans.", plafond_eur: null, regime_fiscal: "Usufruitier : perçoit les loyers et est imposé en revenus fonciers pendant la durée du démembrement. Nu-propriétaire : aucun revenu ni imposition pendant la période, récupère la pleine propriété sans fiscalité à l'extinction ; non redevable de l'IFI sur la valeur du bien démembré pendant la durée (sauf exceptions)", supports: [] },
|
||||
];"""
|
||||
|
||||
content = content.replace(anchor_last_item, new_items, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Dette privée / FPCI-SLP-FPS / Infrastructures / GFF / GFV / LMNP-LMP / SCPI démembrement inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,47 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
anchor = "\nexport default db;\n"
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
block = '''
|
||||
// Extension du 29/09/26 (suite) — support "ETC" (demande Olivier, à partir d'une capture d'écran
|
||||
// d'un contrat d'assurance-vie distinguant "+200 ETF & 22 ETC" parmi ses supports) : un Exchange
|
||||
// Traded Commodity (ETC) est un instrument coté qui réplique le cours d'une matière première
|
||||
// (or, pétrole...), proche d'un ETF dans sa mécanique de cotation mais sans statut UCITS/OPCVM —
|
||||
// distinct du support "ETF / trackers" (fonds indiciels UCITS) et de "Matières premières / métaux
|
||||
// précieux" (autre modalité d'exposition aux matières premières, ex. en unités de compte
|
||||
// d'assurance-vie). Rattaché aux mêmes enveloppes que "Fonds monétaires" ci-dessus (menu de
|
||||
// supports déjà large) ; PEA/PEA-PME exclus, comme en pratique un ETC matières premières
|
||||
// physiques n'est pas éligible au PEA. Toujours exécuté (INSERT OR IGNORE, clés UNIQUE sur nom).
|
||||
{
|
||||
db.prepare("INSERT OR IGNORE INTO supports_referentiel (nom, description) VALUES (?, ?)").run(
|
||||
'ETC (Exchange Traded Commodities)',
|
||||
"Tracker coté sur une matière première (or, pétrole, panier de matières premières...) — proche de l'ETF dans sa mécanique de cotation, mais sans statut UCITS/OPCVM ; distinct du support « ETF / trackers » et de « Matières premières / métaux précieux »."
|
||||
);
|
||||
const getSuppRefIdEtc = db.prepare('SELECT id FROM supports_referentiel WHERE nom = ?');
|
||||
const suppEtc = getSuppRefIdEtc.get('ETC (Exchange Traded Commodities)');
|
||||
if (suppEtc) {
|
||||
const getEnvRefIdEtc = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?');
|
||||
const linkEnvSuppEtc = db.prepare('INSERT OR IGNORE INTO enveloppes_supports_disponibles (enveloppe_id, support_id) VALUES (?, ?)');
|
||||
for (const envNom of [
|
||||
'Compte-titres ordinaire (CTO)', 'Assurance-vie multisupport', 'PER individuel',
|
||||
'PER collectif / obligatoire (PERCOL, article 83)', 'Contrat de capitalisation',
|
||||
'Assurance-vie luxembourgeoise',
|
||||
]) {
|
||||
const env = getEnvRefIdEtc.get(envNom);
|
||||
if (env) linkEnvSuppEtc.run(env.id, suppEtc.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
'''
|
||||
|
||||
content = content.replace(anchor, block, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Support ETC inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,84 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1) Insérer la liste des 11 supports (dupliquée localement, hors de portée du AV_PER_SUPPORTS
|
||||
# du bloc de seed initial) juste avant EXTRA_ENVELOPPES.
|
||||
anchor_supports = " const EXTRA_ENVELOPPES = [\n"
|
||||
assert content.count(anchor_supports) == 1, f"anchor_supports count={content.count(anchor_supports)}"
|
||||
supports_const = """ const AV_PER_SUPPORTS_EXTRA = [
|
||||
'Fonds en euros', 'OPCVM / fonds actifs', 'ETF / trackers', 'Titres vifs (actions)',
|
||||
'Obligations / fonds datés', 'Immobilier papier (SCPI/SCI/OPCI)', 'Private equity (FCPR)',
|
||||
'Produits structurés', 'Matières premières / métaux précieux', 'Gestion pilotée / mandat',
|
||||
'Fonds solidaires (90/10)',
|
||||
];
|
||||
|
||||
"""
|
||||
content = content.replace(anchor_supports, supports_const + anchor_supports, 1)
|
||||
|
||||
# 2) Ajouter les nouvelles enveloppes à la fin du tableau EXTRA_ENVELOPPES : Contrat de
|
||||
# capitalisation, Assurance-vie luxembourgeoise, Girardin (demande du 29/09/26 sur les
|
||||
# enveloppes manquantes / actifs "exotiques"), et Wallet crypto conservé par un tiers
|
||||
# (demande du 29/09/26, distincte de l'auto-conservation et du compte sur exchange).
|
||||
anchor_last_item = """ { nom: 'Participation directe non cotée (startups/PME)', categories: ['Financement participatif / private equity'], description: "Prise de participation directe au capital d'une société non cotée, hors plateforme crowdequity.", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values (ou barème) ; réduction d'IR à l'entrée possible sous conditions (IR-PME/Madelin)", supports: [] },
|
||||
];"""
|
||||
assert content.count(anchor_last_item) == 1, f"anchor_last_item count={content.count(anchor_last_item)}"
|
||||
|
||||
new_items = """ { nom: 'Participation directe non cotée (startups/PME)', categories: ['Financement participatif / private equity'], description: "Prise de participation directe au capital d'une société non cotée, hors plateforme crowdequity.", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values (ou barème) ; réduction d'IR à l'entrée possible sous conditions (IR-PME/Madelin)", supports: [] },
|
||||
{ nom: 'Contrat de capitalisation', categories: ['Assurance-vie'], description: "Support d'épargne proche de l'assurance-vie (fonds euros + unités de compte), mais sans clause bénéficiaire ni dénouement au décès — entre dans la succession classique. Peut être souscrit par une personne physique ou une personne morale (société, SCI...).", plafond_eur: null, regime_fiscal: "Fiscalité des gains identique à l'assurance-vie pour un particulier (abattement après 8 ans, PFU ou PFL) ; pas de régime de succession spécifique (articles 990 I / 757 B) ; transmissible par donation du vivant, contrairement à l'assurance-vie", supports: AV_PER_SUPPORTS_EXTRA },
|
||||
{ nom: 'Assurance-vie luxembourgeoise', categories: ['Assurance-vie'], description: "Contrat d'assurance-vie souscrit auprès d'un assureur luxembourgeois — triangle de sécurité (actifs ségrégués auprès d'une banque dépositaire distincte de l'assureur), univers d'investissement et devises généralement plus larges qu'un contrat français.", plafond_eur: null, regime_fiscal: "Fiscalité identique à l'assurance-vie française pour un résident fiscal français (abattement après 8 ans, PFU/PFL) ; déclaration du contrat étranger requise (formulaire 3916-3916 bis)", supports: AV_PER_SUPPORTS_EXTRA },
|
||||
{ nom: 'Girardin (industriel / social)', categories: ['Financement participatif / private equity'], description: "Investissement dans un dispositif de défiscalisation outre-mer — financement d'un investissement productif ou de logement social en DOM-TOM, en échange d'une réduction d'impôt immédiate. Capital non récupéré (pas de valeur résiduelle), contrairement à un FCPI/FIP.", plafond_eur: null, regime_fiscal: "Réduction d'IR l'année suivant l'investissement, généralement supérieure au montant investi (effet de levier) ; plafonnement spécifique des niches fiscales outre-mer ; risque de remise en cause fiscale si le monteur/exploitant ne respecte pas ses engagements (durée de conservation 5 ans)", supports: [] },
|
||||
{ nom: 'Wallet crypto (conservation par un tiers)', categories: ['Actifs numériques'], description: "Cryptomonnaies détenues dans un wallet dont les clés sont conservées par un tiers de confiance (service de custody dédié, family office...), par opposition à l'auto-conservation ou à un compte sur une plateforme d'échange.", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086", supports: [] },
|
||||
];"""
|
||||
|
||||
content = content.replace(anchor_last_item, new_items, 1)
|
||||
|
||||
# 3) Étendre la boucle de traitement pour gérer aussi les supports (jusqu'ici uniquement les
|
||||
# catégories, car aucune des 7 enveloppes du 29/09/26 n'avait de support).
|
||||
anchor_loop = """ const insEnvRefExtra = db.prepare('INSERT OR IGNORE INTO enveloppes_referentiel (nom, description, plafond_eur, regime_fiscal) VALUES (?, ?, ?, ?)');
|
||||
const getEnvRefIdExtra = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?');
|
||||
const getCatEnvIdExtra = db.prepare('SELECT id FROM categories_enveloppes WHERE nom = ?');
|
||||
const linkEnvCatExtra = db.prepare('INSERT OR IGNORE INTO referentiel_categories_enveloppes (enveloppe_id, categorie_id) VALUES (?, ?)');
|
||||
db.transaction(() => {
|
||||
for (const { nom, description, plafond_eur, regime_fiscal, categories } of EXTRA_ENVELOPPES) {
|
||||
insEnvRefExtra.run(nom, description ?? null, plafond_eur ?? null, regime_fiscal ?? null);
|
||||
const env = getEnvRefIdExtra.get(nom);
|
||||
if (!env) continue;
|
||||
for (const catNom of categories) {
|
||||
const cat = getCatEnvIdExtra.get(catNom);
|
||||
if (cat) linkEnvCatExtra.run(env.id, cat.id);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}"""
|
||||
assert content.count(anchor_loop) == 1, f"anchor_loop count={content.count(anchor_loop)}"
|
||||
|
||||
new_loop = """ const insEnvRefExtra = db.prepare('INSERT OR IGNORE INTO enveloppes_referentiel (nom, description, plafond_eur, regime_fiscal) VALUES (?, ?, ?, ?)');
|
||||
const getEnvRefIdExtra = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?');
|
||||
const getCatEnvIdExtra = db.prepare('SELECT id FROM categories_enveloppes WHERE nom = ?');
|
||||
const linkEnvCatExtra = db.prepare('INSERT OR IGNORE INTO referentiel_categories_enveloppes (enveloppe_id, categorie_id) VALUES (?, ?)');
|
||||
const getSuppRefIdExtra = db.prepare('SELECT id FROM supports_referentiel WHERE nom = ?');
|
||||
const linkEnvSuppExtra = db.prepare('INSERT OR IGNORE INTO enveloppes_supports_disponibles (enveloppe_id, support_id) VALUES (?, ?)');
|
||||
db.transaction(() => {
|
||||
for (const { nom, description, plafond_eur, regime_fiscal, categories, supports } of EXTRA_ENVELOPPES) {
|
||||
insEnvRefExtra.run(nom, description ?? null, plafond_eur ?? null, regime_fiscal ?? null);
|
||||
const env = getEnvRefIdExtra.get(nom);
|
||||
if (!env) continue;
|
||||
for (const catNom of categories) {
|
||||
const cat = getCatEnvIdExtra.get(catNom);
|
||||
if (cat) linkEnvCatExtra.run(env.id, cat.id);
|
||||
}
|
||||
for (const suppNom of (supports || [])) {
|
||||
const supp = getSuppRefIdExtra.get(suppNom);
|
||||
if (supp) linkEnvSuppExtra.run(env.id, supp.id);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}"""
|
||||
|
||||
content = content.replace(anchor_loop, new_loop, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Extension 2 (contrat de capitalisation, AV luxembourgeoise, Girardin, wallet tiers) inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,71 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
anchor = """db.prepare(`
|
||||
UPDATE enveloppes_referentiel SET plafond_eur = 61200, updated_at = datetime('now')
|
||||
WHERE nom = 'PEL' AND plafond_eur IS NULL
|
||||
`).run();
|
||||
|
||||
export default db;
|
||||
"""
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
block = '''db.prepare(`
|
||||
UPDATE enveloppes_referentiel SET plafond_eur = 61200, updated_at = datetime('now')
|
||||
WHERE nom = 'PEL' AND plafond_eur IS NULL
|
||||
`).run();
|
||||
|
||||
// Extension du 29/09/26 (demande Olivier) : "je ne voudrais pas faire un nième référentiel [...]
|
||||
// il faut peut-être que cela soit des catégories supplémentaires même si ce n'a pas tout à fait
|
||||
// le même statut" — plutôt qu'un 3e référentiel séparé pour les actifs "physiques"/hors
|
||||
// enveloppe fiscale (immobilier direct, crypto, métaux précieux, objets de collection,
|
||||
// participations non cotées), ils sont intégrés au référentiel existant : quelques nouvelles
|
||||
// catégories (familles) dans categories_enveloppes, et des enveloppes rattachées, sur le même
|
||||
// principe que les enveloppes SCPI/OPCI/SCI détenues en direct déjà présentes (aucun support —
|
||||
// détention directe, pas de menu de supports ; aucun institution_id — catalogue seul, comme le
|
||||
// reste de ce référentiel). Les participations non cotées réutilisent la catégorie existante
|
||||
// "Financement participatif / private equity" plutôt que d'en créer une nouvelle (même famille
|
||||
// que le Crowdequity et les FCPI/FIP). Toujours exécuté (INSERT OR IGNORE, clés UNIQUE sur nom)
|
||||
// — s'ajoute sans écraser à un référentiel déjà seedé, comme le backfill CEL/PEL ci-dessus.
|
||||
{
|
||||
const insCatEnvExtra = db.prepare('INSERT OR IGNORE INTO categories_enveloppes (nom) VALUES (?)');
|
||||
for (const nom of ['Immobilier physique', 'Actifs numériques', 'Métaux précieux', 'Objets de collection']) {
|
||||
insCatEnvExtra.run(nom);
|
||||
}
|
||||
|
||||
const EXTRA_ENVELOPPES = [
|
||||
{ nom: 'Résidence principale', categories: ['Immobilier physique'], description: "Logement occupé à titre de résidence principale.", plafond_eur: null, regime_fiscal: "Plus-value de cession exonérée (résidence principale)", supports: [] },
|
||||
{ nom: 'Immobilier locatif (détention directe)', categories: ['Immobilier physique'], description: "Bien immobilier loué, détenu en direct (hors SCPI/SCI).", plafond_eur: null, regime_fiscal: "Revenus fonciers (micro-foncier ou réel) + plus-value immobilière (abattement selon durée de détention)", supports: [] },
|
||||
{ nom: 'Compte crypto (exchange)', categories: ['Actifs numériques'], description: "Cryptomonnaies détenues sur une plateforme d'échange centralisée.", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086", supports: [] },
|
||||
{ nom: 'Wallet crypto (auto-conservation)', categories: ['Actifs numériques'], description: "Cryptomonnaies en auto-conservation (wallet matériel ou logiciel).", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086", supports: [] },
|
||||
{ nom: 'Métaux précieux physiques', categories: ['Métaux précieux'], description: "Or, argent... détenus physiquement (lingots, pièces).", plafond_eur: null, regime_fiscal: "Taxe forfaitaire sur les métaux précieux (11 %), ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat", supports: [] },
|
||||
{ nom: 'Objets de collection', categories: ['Objets de collection'], description: "Montres, art, antiquités...", plafond_eur: null, regime_fiscal: "Taxe forfaitaire sur les objets d'art/collection/antiquité (6 %), ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat", supports: [] },
|
||||
{ nom: 'Participation directe non cotée (startups/PME)', categories: ['Financement participatif / private equity'], description: "Prise de participation directe au capital d'une société non cotée, hors plateforme crowdequity.", plafond_eur: null, regime_fiscal: "Flat tax 30 % sur les plus-values (ou barème) ; réduction d'IR à l'entrée possible sous conditions (IR-PME/Madelin)", supports: [] },
|
||||
];
|
||||
const insEnvRefExtra = db.prepare('INSERT OR IGNORE INTO enveloppes_referentiel (nom, description, plafond_eur, regime_fiscal) VALUES (?, ?, ?, ?)');
|
||||
const getEnvRefIdExtra = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?');
|
||||
const getCatEnvIdExtra = db.prepare('SELECT id FROM categories_enveloppes WHERE nom = ?');
|
||||
const linkEnvCatExtra = db.prepare('INSERT OR IGNORE INTO referentiel_categories_enveloppes (enveloppe_id, categorie_id) VALUES (?, ?)');
|
||||
db.transaction(() => {
|
||||
for (const { nom, description, plafond_eur, regime_fiscal, categories } of EXTRA_ENVELOPPES) {
|
||||
insEnvRefExtra.run(nom, description ?? null, plafond_eur ?? null, regime_fiscal ?? null);
|
||||
const env = getEnvRefIdExtra.get(nom);
|
||||
if (!env) continue;
|
||||
for (const catNom of categories) {
|
||||
const cat = getCatEnvIdExtra.get(catNom);
|
||||
if (cat) linkEnvCatExtra.run(env.id, cat.id);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
export default db;
|
||||
'''
|
||||
|
||||
content = content.replace(anchor, block, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Extension actifs directs inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,58 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
anchor = """ })();
|
||||
}
|
||||
|
||||
export default db;
|
||||
"""
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
block = ''' })();
|
||||
}
|
||||
|
||||
// Extension du 29/09/26 (demande Olivier, à propos de produits Spiko/Regular Finance) : "sais-tu
|
||||
// comment les classifier ?" — Spiko et Regular Finance ne sont pas des enveloppes mais des
|
||||
// PLATEFORMES/INSTITUTIONS donnant accès à un fonds monétaire (OPCVM monétaire, y compris
|
||||
// lorsqu'il est tokenisé sur blockchain comme chez Spiko) : elles ont vocation à rejoindre
|
||||
// institutions_referentiel (catégorie à créer, ex. "Gestion de trésorerie"), pas
|
||||
// enveloppes_referentiel — le rattachement viendra en Phase 3. Ce qui manquait réellement à CE
|
||||
// référentiel, c'est le SUPPORT lui-même : "OPCVM / fonds actifs" existant désigne des fonds
|
||||
// actions/obligations/diversifiés gérés activement, pas un fonds monétaire (quasi-cash, duration
|
||||
// très courte, profil de risque très différent) — les deux ne doivent pas être confondus.
|
||||
// Toujours exécuté (INSERT OR IGNORE, clés UNIQUE sur nom), même principe que les extensions
|
||||
// précédentes.
|
||||
{
|
||||
db.prepare("INSERT OR IGNORE INTO supports_referentiel (nom, description) VALUES (?, ?)").run(
|
||||
'Fonds monétaires (OPCVM monétaire)',
|
||||
"Fonds monétaire classique ou tokenisé sur blockchain (ex. Spiko, Regular Finance) — quasi-cash, duration très courte, distinct d'un OPCVM actions/obligations/diversifié."
|
||||
);
|
||||
const getSuppRefIdMonetaire = db.prepare('SELECT id FROM supports_referentiel WHERE nom = ?');
|
||||
const suppMonetaire = getSuppRefIdMonetaire.get('Fonds monétaires (OPCVM monétaire)');
|
||||
if (suppMonetaire) {
|
||||
const getEnvRefIdMonetaire = db.prepare('SELECT id FROM enveloppes_referentiel WHERE nom = ?');
|
||||
const linkEnvSuppMonetaire = db.prepare('INSERT OR IGNORE INTO enveloppes_supports_disponibles (enveloppe_id, support_id) VALUES (?, ?)');
|
||||
// Enveloppes où un fonds monétaire a sa place (menu de supports déjà large, cohérent avec un
|
||||
// placement défensif) — PEA/PEA-PME exclus (non éligibles réglementairement), enveloppes
|
||||
// "cash" (Livret A, comptes courants...) exclues (déjà sans menu de supports par nature).
|
||||
for (const envNom of [
|
||||
'Compte-titres ordinaire (CTO)', 'Assurance-vie multisupport', 'PER individuel',
|
||||
'PER collectif / obligatoire (PERCOL, article 83)', 'Contrat de capitalisation',
|
||||
'Assurance-vie luxembourgeoise',
|
||||
]) {
|
||||
const env = getEnvRefIdMonetaire.get(envNom);
|
||||
if (env) linkEnvSuppMonetaire.run(env.id, suppMonetaire.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
'''
|
||||
|
||||
content = content.replace(anchor, block, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Fonds monétaires support + links inserted OK, new length:", len(content))
|
||||
@@ -0,0 +1,53 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1) Retrait de "Gestion pilotée / mandat" du seed initial des supports (SEED_SUPPORTS) — ne
|
||||
# concerne que les installations neuves, ce bloc étant gardé par COUNT()===0.
|
||||
old_seed_line = " { nom: 'Gestion pilotée / mandat', description: 'Allocation déléguée à un gérant selon un profil de risque — mode par défaut de nombreux PER.' },\n"
|
||||
assert content.count(old_seed_line) == 1, f"old_seed_line count={content.count(old_seed_line)}"
|
||||
content = content.replace(old_seed_line, "", 1)
|
||||
|
||||
# 2) Retrait de 'Gestion pilotée / mandat' des deux listes de supports "classiques" AV/PER
|
||||
# (AV_PER_SUPPORTS et AV_PER_SUPPORTS_EXTRA) — présent dans les deux car dupliqué (la seconde
|
||||
# liste, hors de portée du bloc de seed initial, a été recopiée lors de l'extension du 29/09/26
|
||||
# pour Contrat de capitalisation / Assurance-vie luxembourgeoise).
|
||||
old_list_fragment = "'Produits structurés', 'Matières premières / métaux précieux', 'Gestion pilotée / mandat',"
|
||||
assert content.count(old_list_fragment) == 2, f"old_list_fragment count={content.count(old_list_fragment)}"
|
||||
new_list_fragment = "'Produits structurés', 'Matières premières / métaux précieux',"
|
||||
content = content.replace(old_list_fragment, new_list_fragment) # replace_all (2 occurrences)
|
||||
|
||||
# 3) Nouveau bloc toujours exécuté : supprime la ligne "Gestion pilotée / mandat" pour les
|
||||
# installations déjà seedées (le retrait du texte source ci-dessus ne corrige que les
|
||||
# installations neuves). ON DELETE CASCADE sur enveloppes_supports_disponibles nettoie
|
||||
# automatiquement les liens existants vers ce support — c'est un pur catalogue (aucune donnée
|
||||
# d'investissement réelle n'y est aujourd'hui rattachée), donc la suppression est sans risque.
|
||||
# Toujours exécuté et idempotent (DELETE sans WHERE de garde spécifique au-delà du nom — un
|
||||
# deuxième passage ne trouve simplement plus rien à supprimer).
|
||||
anchor = "\nexport default db;\n"
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
block = '''
|
||||
// Correctif du 29/09/26 (suite) — retrait du support "Gestion pilotée / mandat" (demande
|
||||
// Olivier, à propos d'une capture d'écran distinguant "3 modes de gestion" — libre / pilotée /
|
||||
// mixte — d'un contrat d'assurance-vie) : le "mode de gestion" décrit QUI décide de l'allocation
|
||||
// (le titulaire, ou un gérant délégué selon un mandat), c'est une dimension orthogonale au
|
||||
// SUPPORT (le type d'instrument financier détenu — ETF, actions, SCPI...). Une gestion pilotée
|
||||
// s'applique justement à un panier de vrais supports (fonds euros, fonds datés, SCPI, produits
|
||||
// structurés...), elle n'en est pas un elle-même. Cette ligne datait du seed initial du 28/09/26
|
||||
// et n'aurait pas dû être modélisée comme support ; elle est retirée du référentiel et de la
|
||||
// liste de supports "classiques" AV/PER (voir plus haut). Toujours exécuté et idempotent —
|
||||
// DELETE cascade automatiquement les liens vers cette entrée (enveloppes_supports_disponibles,
|
||||
// ON DELETE CASCADE) ; aucune donnée d'investissement réelle n'est rattachée à ce catalogue à ce
|
||||
// stade, la suppression est donc sans risque de perte de données utilisateur.
|
||||
db.prepare("DELETE FROM supports_referentiel WHERE nom = 'Gestion pilotée / mandat'").run();
|
||||
|
||||
export default db;
|
||||
'''
|
||||
|
||||
content = content.replace(anchor, block, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Gestion pilotée / mandat removed from supports OK, new length:", len(content))
|
||||
@@ -0,0 +1,102 @@
|
||||
path = "backend/src/db/index.js"
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Liste des 12 lignes à corriger : (nom, ancien texte exact regime_fiscal, nouveau texte)
|
||||
FIXES = [
|
||||
('Compte courant rémunéré',
|
||||
'Flat tax 30 %',
|
||||
'Flat tax (PFU)'),
|
||||
('Compte rémunéré nouvelle génération (fintech)',
|
||||
'Flat tax 30 %',
|
||||
'Flat tax (PFU)'),
|
||||
('Livret bancaire non réglementé',
|
||||
'Flat tax 30 % (ou option barème)',
|
||||
'Flat tax (PFU) ou option barème'),
|
||||
('Compte à terme (CAT)',
|
||||
'Flat tax 30 % (ou option barème)',
|
||||
'Flat tax (PFU) ou option barème'),
|
||||
('Compte-titres ordinaire (CTO)',
|
||||
"Flat tax 30 % (ou option barème), pas d'avantage fiscal",
|
||||
"Flat tax (PFU) ou option barème, pas d'avantage fiscal"),
|
||||
('Crowdfunding immobilier',
|
||||
'Fiscalité classique sur les intérêts (flat tax 30 % ou barème)',
|
||||
'Fiscalité classique sur les intérêts (flat tax / PFU, ou option barème)'),
|
||||
('Compte crypto (exchange)',
|
||||
"Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086",
|
||||
"Flat tax (PFU) sur les plus-values de cession (particuliers), déclaration 2086"),
|
||||
('Wallet crypto (auto-conservation)',
|
||||
"Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086",
|
||||
"Flat tax (PFU) sur les plus-values de cession (particuliers), déclaration 2086"),
|
||||
('Métaux précieux physiques',
|
||||
"Taxe forfaitaire sur les métaux précieux (11 %), ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat",
|
||||
"Taxe forfaitaire sur les métaux précieux, ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat"),
|
||||
('Objets de collection',
|
||||
"Taxe forfaitaire sur les objets d'art/collection/antiquité (6 %), ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat",
|
||||
"Taxe forfaitaire sur les objets d'art/collection/antiquité, ou option pour le régime des plus-values sur biens meubles si justificatifs d'achat"),
|
||||
('Participation directe non cotée (startups/PME)',
|
||||
"Flat tax 30 % sur les plus-values (ou barème) ; réduction d'IR à l'entrée possible sous conditions (IR-PME/Madelin)",
|
||||
"Flat tax (PFU) sur les plus-values (ou option barème) ; réduction d'IR à l'entrée possible sous conditions (IR-PME/Madelin)"),
|
||||
('Wallet crypto (conservation par un tiers)',
|
||||
"Flat tax 30 % sur les plus-values de cession (particuliers), déclaration 2086",
|
||||
"Flat tax (PFU) sur les plus-values de cession (particuliers), déclaration 2086"),
|
||||
]
|
||||
|
||||
# 1) Réécriture du texte source, ligne par ligne (chaque ligne contient "nom: '<X>'," ce qui la
|
||||
# rend unique même quand deux lignes partagent le même ancien texte regime_fiscal).
|
||||
lines = content.split("\n")
|
||||
touched = {nom: False for nom, _, _ in FIXES}
|
||||
for i, line in enumerate(lines):
|
||||
for nom, old_txt, new_txt in FIXES:
|
||||
needle_nom = f"nom: '{nom}'"
|
||||
if needle_nom in line and old_txt in line and not touched[nom]:
|
||||
assert line.count(old_txt) == 1, f"{nom}: old_txt found {line.count(old_txt)} times on its line"
|
||||
lines[i] = line.replace(old_txt, new_txt, 1)
|
||||
touched[nom] = True
|
||||
break
|
||||
|
||||
missing = [nom for nom, ok in touched.items() if not ok]
|
||||
assert not missing, f"lignes non trouvées pour : {missing}"
|
||||
content = "\n".join(lines)
|
||||
|
||||
# 2) Bloc de backfill toujours exécuté (INSERT OR IGNORE ne corrige pas le texte des lignes déjà
|
||||
# présentes) : UPDATE ... WHERE nom = ? AND regime_fiscal = '<ancien texte exact>' — ne touche
|
||||
# jamais une ligne dont regime_fiscal a été modifié manuellement (le texte ne matchera plus).
|
||||
anchor = "\nexport default db;\n"
|
||||
assert content.count(anchor) == 1, f"anchor count={content.count(anchor)}"
|
||||
|
||||
backfill_stmts = []
|
||||
for nom, old_txt, new_txt in FIXES:
|
||||
nom_js = nom.replace("\\", "\\\\").replace("'", "\\'")
|
||||
old_js = old_txt.replace("\\", "\\\\").replace("'", "\\'")
|
||||
new_js = new_txt.replace("\\", "\\\\").replace("'", "\\'")
|
||||
backfill_stmts.append(
|
||||
" db.prepare(\n"
|
||||
" \"UPDATE enveloppes_referentiel SET regime_fiscal = ?, updated_at = datetime('now') \" +\n"
|
||||
" \"WHERE nom = ? AND regime_fiscal = ?\"\n"
|
||||
f" ).run('{new_js}', '{nom_js}', '{old_js}');"
|
||||
)
|
||||
|
||||
block = (
|
||||
"\n"
|
||||
"// Correctif du 27/09/26 (demande Olivier : \"Tu alimente la description avec un flat tax à\n"
|
||||
"// 30%, mais ce n'est plus vrai en 2026... évite de mettre le taux actuel qui peut varier\") —\n"
|
||||
"// les taux d'imposition précis (flat tax 30 %, taxes forfaitaires 11 %/6 %...) changent dans\n"
|
||||
"// le temps et rien ici ne les tient à jour : les textes ci-dessus (SEED_ENVELOPPES /\n"
|
||||
"// EXTRA_ENVELOPPES) ont été réécrits pour décrire le MÉCANISME fiscal (flat tax/PFU, taxe\n"
|
||||
"// forfaitaire...) sans figer de pourcentage. Comme INSERT OR IGNORE ne corrige pas le texte\n"
|
||||
"// d'une ligne déjà présente, ce backfill patche les installations déjà seedées — toujours\n"
|
||||
"// exécuté, guardé par l'ancien texte exact pour ne jamais écraser une modification manuelle\n"
|
||||
"// faite depuis l'admin (même principe que le backfill des plafonds CEL/PEL).\n"
|
||||
"{\n"
|
||||
+ "\n".join(backfill_stmts) + "\n"
|
||||
"}\n"
|
||||
"\nexport default db;\n"
|
||||
)
|
||||
|
||||
content = content.replace(anchor, block, 1)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
print("Regime fiscal genericization + backfill inserted OK, new length:", len(content))
|
||||
Reference in new issue
Block a user