Checkpoint: Correction robuste des doublons de factures : empreinte SHA-256 unique des PDF sources, détection globale avant import manuel/email/dossier/web, verrou contre les vérifications IMAP concurrentes, attente réelle de fin de traitement avant marquage lu, migration et tests de non-régression.

This commit is contained in:
Manus
2026-08-22 10:31:54 +00:00
parent d8b2a8fe6f
commit d729a94a96
12 changed files with 2637 additions and 82 deletions

View File

@@ -17,7 +17,8 @@ import { startEmailImportService } from "../emailImportService";
import { startFolderImportService } from "../folderImportService";
import { handleAzureCallback, isAzureAdConfigured, generateToken, verifyToken } from "../auth";
import { createDatabaseBackup } from "../databaseBackup";
import { generateStorageKey, localStoragePut } from "../localStorage";
import { generateStorageKey, localStorageDelete, localStoragePut } from "../localStorage";
import { calculateFileSha256 } from "../fileFingerprint";
const MAX_WEB_IMPORT_BYTES = 20 * 1024 * 1024;
@@ -305,7 +306,7 @@ async function startServer() {
return;
}
const { getWebImportSourceByToken, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile } = await import('../db');
const { getWebImportSourceByToken, createInvoice, findDuplicateInvoice, isInvoiceBlacklisted, updateWebImportSourceStatus, createSourceFile, getSourceFileByContentHash } = await import('../db');
const source = await getWebImportSourceByToken(apiToken);
if (!source) {
res.status(401).json({ error: "Token invalide" });
@@ -317,15 +318,35 @@ async function startServer() {
return;
}
const contentHash = calculateFileSha256(pdfBuffer);
const existingSource = await getSourceFileByContentHash(contentHash);
if (existingSource) {
await updateWebImportSourceStatus(source.id, "success", 0, true);
res.json({ success: true, imported: 0, duplicates: 1, total: 1 });
return;
}
// Stocker d'abord le PDF de façon persistante, comme les autres sources d'import.
const storageKey = generateStorageKey(source.userId, safeFileName);
const { url: fileUrl } = await localStoragePut(storageKey, pdfBuffer, "application/pdf");
const sourceFile = await createSourceFile({
userId: source.userId,
fileName: safeFileName,
fileKey: storageKey,
fileUrl,
});
let sourceFile;
try {
sourceFile = await createSourceFile({
userId: source.userId,
fileName: safeFileName,
fileKey: storageKey,
fileUrl,
contentHash,
});
} catch (error: any) {
if (error?.code === "ER_DUP_ENTRY" || error?.errno === 1062) {
await localStorageDelete(storageKey).catch(() => undefined);
await updateWebImportSourceStatus(source.id, "success", 0, true);
res.json({ success: true, imported: 0, duplicates: 1, total: 1 });
return;
}
throw error;
}
const userSettings = await getUserSettings(source.userId);
const aiSettings = {
aiProvider: userSettings?.aiProvider || "manus",