Mise en place de la notion de perte définitive, ainsi que la révision avec statut Polongation

This commit is contained in:
ocroguennec committed 2026-08-22 16:27:41 +02:00
1 parent 4ad6ea79e8
commit 8e1b82d161
21 files changed
+1069 -184

No files matched your search

+151
View File
@@ -464,6 +464,21 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_revisions_inv ON investissement_revision
}
}
// ── Migration : traçage du statut lors d'une révision (fonctionnalité Prolongation) ──
// Quand une révision change la date cible d'un dossier en_retard/procedure, le statut
// bascule automatiquement à 'prolongation' (cf. routes/investissements.js). On trace
// l'ancien et le nouveau statut ici, comme ancien_taux/nouveau_taux, pour permettre un
// rollback fidèle (DELETE /revisions/:rid restaure ancien_statut).
{
const revColsStatut = db.prepare('PRAGMA table_info(investissement_revisions)').all().map(c => c.name);
if (!revColsStatut.includes('ancien_statut')) {
db.exec('ALTER TABLE investissement_revisions ADD COLUMN ancien_statut TEXT');
}
if (!revColsStatut.includes('nouveau_statut')) {
db.exec('ALTER TABLE investissement_revisions ADD COLUMN nouveau_statut TEXT');
}
}
// ── Migration : rôle utilisateur ─────────────────────────────────────────
{
const userCols = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
@@ -2381,4 +2396,140 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)');
}
}
// ── Migration : ajout du statut 'perte_definitive' ──────────────────────
// Nouvelle valeur d'enum pour déclarer explicitement qu'un capital est
// définitivement perdu (irrécouvrable), distincte de 'en_retard'/'procedure'
// qui restent potentiellement recouvrables. SQLite ne permet pas d'altérer un
// CHECK constraint : on recrée la table avec le nouveau CHECK, même stratégie
// que la migration 'defaut' → 'en_retard' plus haut. Placé en toute fin de
// fichier (après toutes les migrations ADD COLUMN de la table investissements)
// pour que PRAGMA table_info reflète bien la totalité des colonnes, y compris
// sur une base fraîchement créée depuis schema.sql.
{
const schemaInvPerte = db.prepare(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='investissements'"
).get()?.sql ?? '';
if (schemaInvPerte && !schemaInvPerte.includes("'perte_definitive'")) {
const tempName = '__repair_investissements_perte';
// Le nom de table peut être stocké avec ou sans guillemets dans
// sqlite_master.sql selon qu'une précédente migration (ex. 'defaut' →
// 'en_retard') a déjà renommé la table via ALTER TABLE ... RENAME TO
// (SQLite conserve alors le nom entre guillemets dans le DDL stocké) —
// le regex doit couvrir les deux formes, sinon le remplacement échoue
// silencieusement et fixedDdl retente de créer "investissements" alors
// qu'elle existe déjà (SqliteError: table "investissements" already exists).
const fixedDdl = schemaInvPerte
.replace(/CREATE TABLE\s+"?investissements"?/, `CREATE TABLE "${tempName}"`)
.replace(",'cloture'))", ",'cloture','perte_definitive'))");
if (fixedDdl.includes(`"${tempName}"`) && fixedDdl.includes("'perte_definitive'")) {
const colDefs = db.prepare('PRAGMA table_info(investissements)').all();
const colNames = colDefs.map(c => `"${c.name}"`).join(', ');
const idxs = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='investissements' 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 investissements`);
for (const idx of idxs) db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
db.exec('DROP TABLE investissements');
// 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 investissements`);
db.exec('PRAGMA legacy_alter_table = OFF');
db.exec('PRAGMA foreign_keys = ON');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_inv ON investissements(investisseur_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_plat ON investissements(plateforme_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_statut ON investissements(statut)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_date ON investissements(date_souscription)');
console.log("[DB] investissements: statut 'perte_definitive' ajouté au CHECK constraint");
} else {
console.error("[DB] Migration perte_definitive : nom de table ou motif ',cloture))' introuvable dans le DDL — migration ignorée, à vérifier manuellement");
}
}
}
// ── Migration : table investissement_pertes ──────────────────────────────
// Trace chaque déclaration de perte définitive : date de constat, montant
// perdu (préfilli côté frontend avec le capital restant dû mais éditable pour
// une perte partielle si une partie du capital a déjà été recouvrée) et motif
// obligatoire. Distincte de investissement_historique (piste d'audit générique
// auto-détectée) : ici on conserve la donnée structurée exploitée par le
// calcul fiscal (case 2TY) et par les KPI de capital investi/restant dû.
db.exec(`
CREATE TABLE IF NOT EXISTS investissement_pertes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
investissement_id INTEGER NOT NULL REFERENCES investissements(id) ON DELETE CASCADE,
date_effet TEXT NOT NULL,
montant_perte REAL NOT NULL CHECK(montant_perte >= 0),
ancien_statut TEXT,
motif TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`);
db.exec('CREATE INDEX IF NOT EXISTS idx_pertes_inv ON investissement_pertes(investissement_id)');
// ── Migration : ajout du statut 'prolongation' ──────────────────────────
// Nouvelle valeur d'enum posée automatiquement par POST /revisions quand une révision
// des conditions change la date cible d'un dossier en_retard/procedure (renégociation
// avec échéancier modifié) : le dossier sort temporairement du scope "à risque" jusqu'à
// ce que la nouvelle date cible soit à son tour dépassée (cf. jobs/autoStatut.js qui le
// repasse alors en_retard). Même stratégie de recréation de table que 'perte_definitive'
// ci-dessus (SQLite ne permet pas d'altérer un CHECK constraint). Placé après cette
// dernière migration pour bénéficier du même schemaInv déjà à jour (CHECK contenant
// déjà 'perte_definitive' à ce stade, sur une base existante comme sur une base neuve).
{
const schemaInvProlong = db.prepare(
"SELECT sql FROM sqlite_master WHERE type='table' AND name='investissements'"
).get()?.sql ?? '';
if (schemaInvProlong && !schemaInvProlong.includes("'prolongation'")) {
const tempName = '__repair_investissements_prolongation';
// Cf. migration perte_definitive plus haut : le nom de table peut être stocké
// avec ou sans guillemets dans sqlite_master.sql selon l'historique des ALTER
// TABLE ... RENAME TO déjà exécutés — le regex doit couvrir les deux formes.
const fixedDdl = schemaInvProlong
.replace(/CREATE TABLE\s+"?investissements"?/, `CREATE TABLE "${tempName}"`)
.replace(",'perte_definitive'))", ",'perte_definitive','prolongation'))");
if (fixedDdl.includes(`"${tempName}"`) && fixedDdl.includes("'prolongation'")) {
const colDefs = db.prepare('PRAGMA table_info(investissements)').all();
const colNames = colDefs.map(c => `"${c.name}"`).join(', ');
const idxs = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='investissements' 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 investissements`);
for (const idx of idxs) db.exec(`DROP INDEX IF EXISTS "${idx.name}"`);
db.exec('DROP TABLE investissements');
// 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 investissements`);
db.exec('PRAGMA legacy_alter_table = OFF');
db.exec('PRAGMA foreign_keys = ON');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_inv ON investissements(investisseur_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_plat ON investissements(plateforme_id)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_statut ON investissements(statut)');
db.exec('CREATE INDEX IF NOT EXISTS idx_inv_date ON investissements(date_souscription)');
console.log("[DB] investissements: statut 'prolongation' ajouté au CHECK constraint");
} else {
console.error("[DB] Migration prolongation : nom de table ou motif \",'perte_definitive'))\" introuvable dans le DDL — migration ignorée, à vérifier manuellement");
}
}
}
export default db;
+2 -2
View File
@@ -87,7 +87,7 @@ CREATE INDEX IF NOT EXISTS idx_depret_date ON depots_retraits(date_operation);
-- ---------------------------------------------------------------------
-- INVESTISSEMENTS (CF Investissements - liste des projets souscrits)
-- statut: 'en_cours' | 'rembourse' | 'en_retard' | 'procedure' | 'cloture'
-- statut: 'en_cours' | 'rembourse' | 'en_retard' | 'procedure' | 'cloture' | 'perte_definitive' | 'prolongation'
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS investissements (
id INTEGER PRIMARY KEY,
@@ -104,7 +104,7 @@ CREATE TABLE IF NOT EXISTS investissements (
type_remb TEXT, -- 'in_fine' | 'amortissable' | 'differe'
freq_interets TEXT NOT NULL DEFAULT 'mensuel', -- 'mensuel' | 'trimestriel' | 'in_fine'
statut TEXT NOT NULL DEFAULT 'en_cours'
CHECK(statut IN ('en_cours','rembourse','en_retard','procedure','cloture')),
CHECK(statut IN ('en_cours','rembourse','en_retard','procedure','cloture','perte_definitive','prolongation')),
reference TEXT, -- ID projet sur la plateforme
source TEXT NOT NULL DEFAULT 'manuel',
notes TEXT,