From eecbd07b5c4acaab641b3db0cf51435e444665d8 Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 17 Aug 2026 22:35:40 +0200 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20versionner=20le=20manifeste=20de?= =?UTF-8?q?=20d=C3=A9ploiement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app.json diff --git a/app.json b/app.json new file mode 100644 index 0000000..f9d753d --- /dev/null +++ b/app.json @@ -0,0 +1,13 @@ +{ + "id": "demat-facturation-dsi", + "name": "Démat. Facturation DSI", + "category": "SANTINOVA", + "urls": { + "recette": "https://demat-facturation.recette.santinova-soft.org", + "prod": "https://demat-facturation.santinova-soft.org" + }, + "containerName": "demat-facturation-app", + "image": "images/demat-facturation-dsi.jpg", + "giteaRepo": "demat-facturation", + "giteaOwner": "manus-admin" +} From e39dd38beca0910986897cb7fcc42d5e07397eb7 Mon Sep 17 00:00:00 2001 From: Manus Date: Tue, 18 Aug 2026 07:40:54 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20suivi=20du=20d=C3=A9ploiement=20re?= =?UTF-8?q?cette?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/todo.md b/todo.md index 37593e2..5c5ed6f 100644 --- a/todo.md +++ b/todo.md @@ -702,3 +702,8 @@ - [x] Documenter les modules métier, les invariants et les décisions techniques critiques - [x] Ajouter des tests de non-régression ciblés et vérifier build, types et tests - [x] Normaliser les valeurs OAuth de loginMethod avant écriture en base + +## Déploiement recette — audit de robustesse +- [ ] Pousser le checkpoint d’audit vers Gitea recette +- [ ] Reconstruire l’application sur le serveur de recette +- [ ] Vérifier le commit, les conteneurs et la disponibilité HTTP en recette From de76a761a342bcce66b250eb74d5a4195bd51afd Mon Sep 17 00:00:00 2001 From: Manus Admin Date: Tue, 18 Aug 2026 14:50:31 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20emp=C3=AAcher=20la=20duplication=20m?= =?UTF-8?q?assive=20de=20fichiers=20lors=20de=20l'import=20email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Marquer les emails comme lus immédiatement (markSeen: true) - Vérifier si le fichier existe déjà avant de le stocker (findSourceFileByFileName) - Ajouter un verrou anti-concurrence par utilisateur (runningChecks) - Empêche la création de ~570x doublons par facture --- server/db.ts | 15 +++++++++++++++ server/emailImportService.ts | 19 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/server/db.ts b/server/db.ts index c407a22..f869495 100644 --- a/server/db.ts +++ b/server/db.ts @@ -1218,3 +1218,18 @@ export async function updateWebImportSourceStatus( if (success) update.lastSuccessAt = new Date(); await db.update(webImportSources).set(update).where(eq(webImportSources.id, id)); } + +/** + * Check if a source file with the same fileName already exists for this user + * Used to prevent duplicate file storage during email import + */ +export async function findSourceFileByFileName(userId: number, fileName: string): Promise { + const result = await db.select() + .from(sourceFiles) + .where(and( + eq(sourceFiles.userId, userId), + eq(sourceFiles.fileName, fileName) + )) + .limit(1); + return result[0] || null; +} diff --git a/server/emailImportService.ts b/server/emailImportService.ts index d837854..625209a 100644 --- a/server/emailImportService.ts +++ b/server/emailImportService.ts @@ -9,6 +9,7 @@ import { isInvoiceBlacklisted, createInvoice, createImportLog, + findSourceFileByFileName, } from "./db"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; import { localStoragePut, generateStorageKey } from "./localStorage"; @@ -32,6 +33,8 @@ interface EmailImportConfig { // Store active intervals for each user const activeIntervals = new Map(); +// Verrou anti-concurrence par userId +const runningChecks = new Set(); /** * Process a single email attachment (PDF) @@ -51,6 +54,12 @@ async function processEmailAttachment( console.log(`[EmailImport] File size: ${fileBuffer.length} bytes`); // Store source file + // ANTI-DUPLICATION : vérifier si ce fichier a déjà été importé pour cet utilisateur + const existingSourceFile = await findSourceFileByFileName(userId, fileName); + if (existingSourceFile) { + console.log(`[EmailImport] File ${fileName} already imported for user ${userId} (sourceFile #${existingSourceFile.id}), skipping`); + return { success: true, totalInvoices: 0, imported: 0, duplicates: 1, errors: 0 }; + } const sourceFileKey = generateStorageKey(userId, fileName); console.log(`[EmailImport] Generated storage key: ${sourceFileKey}`); @@ -330,6 +339,12 @@ async function buildImapConfig(config: EmailImportConfig): Promise * Connect to IMAP and process unread emails with PDF attachments */ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { + // Anti-concurrence : ne pas lancer si un check est déjà en cours pour cet utilisateur + if (runningChecks.has(config.userId)) { + console.log(`[EmailImport] Check already running for user ${config.userId}, skipping`); + return; + } + runningChecks.add(config.userId); // Build IMAP config (may involve async OAuth2 token fetch) const imapConfig = await buildImapConfig(config); @@ -380,7 +395,7 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { const fetch = imap.fetch(results, { bodies: "", - markSeen: false, // Don't mark as seen yet + markSeen: true, // Mark as seen immediately to prevent re-processing }); const processedEmails: number[] = []; @@ -474,11 +489,13 @@ async function checkEmailsForPDFs(config: EmailImportConfig): Promise { }); imap.once("error", (err) => { + runningChecks.delete(config.userId); console.error("[EmailImport] IMAP connection error:", err); reject(err); }); imap.once("end", () => { + runningChecks.delete(config.userId); console.log(`[EmailImport] IMAP connection ended for user ${config.userId}`); });