Mise en place de la notion de perte définitive, ainsi que la révision avec statut Polongation
This commit is contained in:
1 parent
4ad6ea79e8
commit
8e1b82d161
21 files changed
+1069
-184
No files matched your search
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user