Mise en place de la feature de gestion des Frais

This commit is contained in:
ocroguennec committed 2026-09-01 22:16:18 +02:00
1 parent 9ea078302d
commit 0334a63139
27 files changed
+2401 -117

No files matched your search

+114
View File
@@ -2613,4 +2613,118 @@ if (!smtpCols.includes('documents_quota_count')) {
}
}
// ── Migration : frais d'opération (Étape 1 — fondations) ────────────────────
// Modélise les frais facturés par certaines plateformes (frais d'entrée,
// frais de gestion, frais sur distribution) : un montant TTC unique par
// événement (pas de détail HT/TVA, non pertinent pour un particulier).
// - remboursements.frais_ttc : montant de frais prélevé sur CE coupon,
// vient réduire la base imposable au même titre que taxe_locale
// (imposition = f(interets_bruts - frais_ttc)), cf. feedback Olivier
// 30/08/26 sur l'avis d'opéré/notice de distribution Fundora (Kyoseil AM).
// - frais_operations : table de traçabilité pour tout frais, qu'il soit
// prélevé "à la source" (souscription ou coupon, aucun mouvement de
// trésorerie réel) ou payé en "versement complémentaire" (porte-monnaie
// de la plateforme ou compte courant du détenteur — dans ce dernier cas
// sans impact sur le solde du porte-monnaie). Pas encore d'UI ni de route
// de création automatique à ce stade : fondations DB + formule fiscale
// uniquement, le reste (étapes souscription/remboursement/modale dédiée)
// viendra dans une prochaine passe.
{
const rembColsFraisTtc = db.prepare('PRAGMA table_info(remboursements)').all().map(c => c.name);
if (!rembColsFraisTtc.includes('frais_ttc')) {
db.exec('ALTER TABLE remboursements ADD COLUMN frais_ttc REAL NOT NULL DEFAULT 0');
}
}
db.exec(`
CREATE TABLE IF NOT EXISTS frais_operations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
investissement_id INTEGER NOT NULL REFERENCES investissements(id) ON DELETE CASCADE,
plateforme_id INTEGER NOT NULL REFERENCES plateformes(id) ON DELETE CASCADE,
remboursement_id INTEGER REFERENCES remboursements(id) ON DELETE CASCADE,
montant REAL NOT NULL,
mode_reglement TEXT NOT NULL DEFAULT 'source'
CHECK(mode_reglement IN ('source','portefeuille','compte_courant')),
compte_id INTEGER REFERENCES comptes(id) ON DELETE SET NULL,
date_operation TEXT NOT NULL,
origine TEXT NOT NULL DEFAULT 'manuel'
CHECK(origine IN ('souscription','remboursement','manuel')),
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_inv ON frais_operations(investissement_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_plat ON frais_operations(plateforme_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_remb ON frais_operations(remboursement_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_date ON frais_operations(date_operation)');
// ── Migration : catégories de frais (31/08/26, demande Olivier) ─────────────
// `origine` (souscription/remboursement/manuel) reste un détail technique (comment la
// ligne a été créée) ; `categorie` devient le classement visible côté utilisateur, utile
// pour le reporting "Frais payés" de l'Étape 5 : souscription (frais d'entrée, unique par
// investissement — lu/écrit uniquement depuis l'étape "Frais" du formulaire de
// création/modification) / gestion / distribution (liée à un remboursement) / autre
// (saisie libre depuis le bloc "Frais d'opération" de la fiche investissement).
{
const fraisOpCols = db.prepare('PRAGMA table_info(frais_operations)').all().map(c => c.name);
if (!fraisOpCols.includes('categorie')) {
db.exec("ALTER TABLE frais_operations ADD COLUMN categorie TEXT NOT NULL DEFAULT 'autre'");
// Rattrapage pour les lignes déjà créées avant l'ajout de cette colonne (tests Olivier,
// 30-31/08/26) : on déduit la catégorie la plus probable depuis `origine`.
db.exec(`
UPDATE frais_operations SET categorie = CASE origine
WHEN 'souscription' THEN 'souscription'
WHEN 'remboursement' THEN 'distribution'
ELSE 'autre'
END
`);
}
}
// ── Migration : mode_reglement 'remboursement' (frais de distribution liés
// automatiquement à un remboursement — Étape 4, 31/08/26) ───────────────────
// SQLite ne permet pas d'altérer un CHECK constraint en place (better-sqlite3
// bloque writable_schema) → même stratégie de reconstruction de table que pour
// investissements/plateformes/remboursements ailleurs dans ce fichier.
{
const schemaFraisOp = db.prepare(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='frais_operations'"
).get()?.sql ?? '';
if (schemaFraisOp.includes("mode_reglement IN ('source','portefeuille','compte_courant')")) {
const tempName = '__repair_frais_operations';
const fixedDdl = schemaFraisOp
.replace(/CREATE TABLE frais_operations\b/, `CREATE TABLE "${tempName}"`)
.replace(
"mode_reglement IN ('source','portefeuille','compte_courant')",
"mode_reglement IN ('source','portefeuille','compte_courant','remboursement')",
);
const colDefs = db.prepare('PRAGMA table_info(frais_operations)').all();
const colNames = colDefs.map(c => `"${c.name}"`).join(', ');
const idxs = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='frais_operations' AND sql IS NOT NULL"
).all();
db.exec('PRAGMA foreign_keys = OFF');
db.exec(`DROP TABLE IF EXISTS "${tempName}"`);
db.exec(fixedDdl);
db.exec(`INSERT INTO "${tempName}" (${colNames}) SELECT ${colNames} FROM frais_operations`);
for (const idx of idxs) db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
db.exec('DROP TABLE frais_operations');
// legacy_alter_table = ON : empêche la réécriture automatique des FK enfants
db.exec('PRAGMA legacy_alter_table = ON');
db.exec(`ALTER TABLE "${tempName}" RENAME TO frais_operations`);
db.exec('PRAGMA legacy_alter_table = OFF');
db.exec('PRAGMA foreign_keys = ON');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_inv ON frais_operations(investissement_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_plat ON frais_operations(plateforme_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_remb ON frais_operations(remboursement_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_fraisop_date ON frais_operations(date_operation)');
}
}
export default db;