diff --git a/backend/src/db/index.js b/backend/src/db/index.js
index e77d582..e0c873b 100644
--- a/backend/src/db/index.js
+++ b/backend/src/db/index.js
@@ -2075,4 +2075,15 @@ console.log('[DB] Migrations 2FA OK');
}
}
+// ── Migration : compteur de doublons ignorés sur imports ──────────────────
+// Distingue les lignes ignorées car identiques à une ligne déjà en base
+// (rows_duplicates) des lignes ignorées pour une autre raison (rows_skipped
+// reste le total ; rows_duplicates est un sous-ensemble informatif).
+{
+ const importsCols = db.prepare('PRAGMA table_info(imports)').all().map(c => c.name);
+ if (!importsCols.includes('rows_duplicates')) {
+ db.exec('ALTER TABLE imports ADD COLUMN rows_duplicates INTEGER NOT NULL DEFAULT 0');
+ }
+}
+
export default db;
diff --git a/backend/src/routes/imports.js b/backend/src/routes/imports.js
index 37f145b..0a5c6fc 100644
--- a/backend/src/routes/imports.js
+++ b/backend/src/routes/imports.js
@@ -137,7 +137,7 @@ router.post('/apply', (req, res, next) => {
invNameMap = new Map(invRows.map(i => [normalizeName(i.nom_projet), i.id]));
}
- let inserted = 0, skipped = 0;
+ let inserted = 0, skipped = 0, duplicates = 0;
const errors = [];
const tx = db.transaction(() => {
@@ -153,22 +153,48 @@ router.post('/apply', (req, res, next) => {
};
if (module === 'depots_retraits') {
+ const plateformeId = resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme');
+ const dateOperation = normaliseDate(v('date_operation'));
+ const type = normaliseType(v('type'));
+ const montant = num(v('montant'));
+
+ // Anti-doublon : même investisseur + plateforme + date + type + montant
+ const dup = db.prepare(`
+ SELECT id FROM depots_retraits
+ WHERE investisseur_id = ? AND plateforme_id = ? AND date_operation = ?
+ AND type = ? AND ABS(montant - ?) < 0.005
+ LIMIT 1
+ `).get(req.investisseur.id, plateformeId, dateOperation, type, montant);
+ if (dup) { duplicates++; continue; }
+
db.prepare(`
INSERT INTO depots_retraits
(investisseur_id, plateforme_id, date_operation, type, montant, libelle, reference, source)
VALUES (?,?,?,?,?,?,?,?)
`).run(
req.investisseur.id,
- resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme'),
- normaliseDate(v('date_operation')),
- normaliseType(v('type')),
- num(v('montant')),
+ plateformeId,
+ dateOperation,
+ type,
+ montant,
v('libelle') || null,
v('reference') || null,
srcLabel,
);
} else if (module === 'investissements') {
+ const plateformeId = resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme');
+ const nomProjet = String(v('nom_projet'));
+ const dateSouscription = normaliseDate(v('date_souscription'));
+
+ // Anti-doublon : même investisseur + plateforme + nom du projet + date de souscription
+ const dup = db.prepare(`
+ SELECT id FROM investissements
+ WHERE investisseur_id = ? AND plateforme_id = ? AND nom_projet = ? AND date_souscription = ?
+ LIMIT 1
+ `).get(req.investisseur.id, plateformeId, nomProjet, dateSouscription);
+ if (dup) { duplicates++; continue; }
+
db.prepare(`
INSERT INTO investissements
(investisseur_id, plateforme_id, nom_projet, emetteur, date_souscription,
@@ -177,10 +203,10 @@ router.post('/apply', (req, res, next) => {
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`).run(
req.investisseur.id,
- resolveRefId(v('plateforme_id'), platIdSet, platNameMap, 'Plateforme'),
- String(v('nom_projet')),
+ plateformeId,
+ nomProjet,
v('emetteur') || null,
- normaliseDate(v('date_souscription')),
+ dateSouscription,
v('date_premiere_echeance') ? normaliseDate(v('date_premiere_echeance')) : null,
v('date_cible') ? normaliseDate(v('date_cible')) : null,
num(v('montant_investi')),
@@ -194,6 +220,8 @@ router.post('/apply', (req, res, next) => {
);
} else if (module === 'remboursements') {
+ const investissementId = resolveRefId(v('investissement_id'), invIdSet, invNameMap, 'Investissement');
+ const dateRemb = normaliseDate(v('date_remb'));
const capital = num(v('capital'));
const cashback = num(v('cashback'));
const bruts = num(v('interets_bruts'));
@@ -201,14 +229,24 @@ router.post('/apply', (req, res, next) => {
const pf = num(v('prelev_forfaitaire'));
const interets_nets = Math.round((bruts - ps - pf) * 100) / 100;
const net_recu = Math.round((capital + cashback + interets_nets) * 100) / 100;
+
+ // Anti-doublon : même investissement + date + capital + intérêts bruts
+ const dup = db.prepare(`
+ SELECT id FROM remboursements
+ WHERE investissement_id = ? AND date_remb = ?
+ AND ABS(capital - ?) < 0.005 AND ABS(interets_bruts - ?) < 0.005
+ LIMIT 1
+ `).get(investissementId, dateRemb, capital, bruts);
+ if (dup) { duplicates++; continue; }
+
db.prepare(`
INSERT INTO remboursements
(investissement_id, date_remb, capital, cashback, interets_bruts, prelev_sociaux,
prelev_forfaitaire, interets_nets, net_recu, statut, source)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
`).run(
- resolveRefId(v('investissement_id'), invIdSet, invNameMap, 'Investissement'),
- normaliseDate(v('date_remb')),
+ investissementId,
+ dateRemb,
capital, cashback, bruts, ps, pf, interets_nets, net_recu,
v('statut') || 'paye',
srcLabel,
@@ -226,8 +264,8 @@ router.post('/apply', (req, res, next) => {
v('url') || null,
v('notes') || null,
);
- // changes = 0 means the row was ignored (nom already exists)
- if (r.changes === 0) throw new Error(`Plateforme "${nom}" existe déjà — ignorée`);
+ // changes = 0 means the row was ignored (nom already exists) — doublon, pas une erreur
+ if (r.changes === 0) { duplicates++; continue; }
} else if (module === 'taux_pfu') {
const annee = parseInt(v('annee'), 10);
@@ -258,8 +296,8 @@ router.post('/apply', (req, res, next) => {
tx();
db.prepare(`
- INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, mapping_json)
- VALUES (?,?,?,?,?,?,?,?)
+ INSERT INTO imports (user_id, investisseur_id, module, filename, rows_total, rows_inserted, rows_skipped, rows_duplicates, mapping_json)
+ VALUES (?,?,?,?,?,?,?,?,?)
`).run(
req.user.id,
req.investisseur?.id ?? null,
@@ -268,13 +306,14 @@ router.post('/apply', (req, res, next) => {
rows.length,
inserted,
skipped,
+ duplicates,
JSON.stringify(mapping),
);
// Clean up temp file
try { fs.unlinkSync(tempPath); } catch { /* */ }
- res.json({ inserted, skipped, total: rows.length, errors: errors.slice(0, 50) });
+ res.json({ inserted, skipped, duplicates, total: rows.length, errors: errors.slice(0, 50) });
} catch (e) { next(e); }
});
diff --git a/frontend/src/pages/settings/ImportsSection.jsx b/frontend/src/pages/settings/ImportsSection.jsx
index aa95bc9..c2db01d 100644
--- a/frontend/src/pages/settings/ImportsSection.jsx
+++ b/frontend/src/pages/settings/ImportsSection.jsx
@@ -406,7 +406,11 @@ export default function ImportsSection() {
});
setResult({
ok: true,
- msg: `✔ Import terminé : ${r.inserted} / ${r.total} lignes insérées${r.skipped > 0 ? `, ${r.skipped} ignorées` : ''}.${r.errors?.length > 0 ? ` (${r.errors.length} avertissement(s))` : ''}`,
+ msg: `✔ Import terminé : ${r.inserted} / ${r.total} lignes insérées`
+ + (r.duplicates > 0 ? `, ${r.duplicates} doublon(s) ignoré(s)` : '')
+ + (r.skipped > 0 ? `, ${r.skipped} ignorée(s)` : '')
+ + '.'
+ + (r.errors?.length > 0 ? ` (${r.errors.length} avertissement(s))` : ''),
});
setPreview(null); setFile(null); setMapping({}); setDefaults({});
api.get('/imports/history').then(setHistory).catch(() => {});
@@ -602,12 +606,12 @@ export default function ImportsSection() {
Date Module Fichier
- Total OK KO
+ Total OK Doublons KO