Maj Phase 2 - Institutions financières

This commit is contained in:
ocroguennec committed 2026-09-26 18:28:15 +02:00
1 parent 1dcb33aa74
commit b18ffc63ee
18 files changed
+383 -16

No files matched your search

+86
View File
@@ -832,4 +832,90 @@ router.post('/exports/:filename/restore', async (req, res, next) => {
}
});
/* ── Comptes orphelins (banque texte libre non reliée au référentiel institutions) ───────
Phase 2 du chantier "Référentiel Institutions financières" (cf. plan de déploiement, doc
projet claude/plan_referentiel_institutions_financieres.md). Contrairement aux plateformes
orphelines (une ligne par plateforme), ici on regroupe par valeur `banque` distincte car
plusieurs comptes (potentiellement de plusieurs utilisateurs) partagent souvent le même
texte libre — Lier/Importer agit donc sur tous les comptes portant cette valeur d'un coup. */
/** Liste les valeurs `banque` distinctes non reliées à institution_id, avec suggestion */
router.get('/comptes-orphelins', (_req, res) => {
const rows = db.prepare(`
SELECT c.banque, u.id AS user_id, u.email AS user_email, u.display_name AS user_display_name, COUNT(*) AS nb
FROM comptes c
JOIN users u ON u.id = c.user_id
WHERE c.institution_id IS NULL AND c.banque IS NOT NULL AND TRIM(c.banque) != ''
GROUP BY c.banque, u.id
ORDER BY c.banque, u.email
`).all();
const byBanque = {};
for (const r of rows) {
if (!byBanque[r.banque]) byBanque[r.banque] = { banque: r.banque, total: 0, users: [] };
byBanque[r.banque].total += r.nb;
byBanque[r.banque].users.push({ id: r.user_id, email: r.user_email, display_name: r.user_display_name, nb: r.nb });
}
const refs = db.prepare('SELECT id, nom FROM institutions_referentiel ORDER BY nom').all();
const enriched = Object.values(byBanque).map(g => {
let bestRef = null, bestScore = 0;
for (const ref of refs) {
const score = similarity(g.banque, ref.nom);
if (score > bestScore) { bestScore = score; bestRef = ref; }
}
return {
...g,
suggestion: bestScore >= SIMILARITY_THRESHOLD
? { institution_id: bestRef.id, institution_nom: bestRef.nom, score: Math.round(bestScore * 100) }
: null,
};
}).sort((a, b) => a.banque.localeCompare(b.banque, 'fr'));
res.json(enriched);
});
/** Lie tous les comptes portant cette valeur `banque` à une institution existante du référentiel */
router.post('/comptes-orphelins/lier', (req, res, next) => {
try {
const { banque, institution_id } = z.object({
banque: z.string().min(1),
institution_id: z.number().int().positive(),
}).parse(req.body);
const ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(institution_id);
if (!ref) throw new HttpError(404, 'Institution introuvable');
const r = db.prepare(
'UPDATE comptes SET institution_id = ? WHERE banque = ? AND institution_id IS NULL'
).run(institution_id, banque);
res.json({ linked: true, nb_comptes: r.changes, institution: ref });
} catch (e) { next(e); }
});
/** Crée une nouvelle institution dans le référentiel à partir de la valeur `banque`, et lie
tous les comptes correspondants (ou lie simplement si une institution de ce nom existe déjà) */
router.post('/comptes-orphelins/importer', (req, res, next) => {
try {
const { banque } = z.object({ banque: z.string().min(1) }).parse(req.body);
let ref = db.prepare('SELECT * FROM institutions_referentiel WHERE nom = ?').get(banque);
let created = false;
if (!ref) {
const r = db.prepare('INSERT INTO institutions_referentiel (nom) VALUES (?)').run(banque);
ref = db.prepare('SELECT * FROM institutions_referentiel WHERE id = ?').get(r.lastInsertRowid);
created = true;
}
const upd = db.prepare(
'UPDATE comptes SET institution_id = ? WHERE banque = ? AND institution_id IS NULL'
).run(ref.id, banque);
res.status(created ? 201 : 200).json({ linked: true, created, nb_comptes: upd.changes, institution: ref });
} catch (e) { next(e); }
});
export default router;