feat: purge import history by retention

This commit is contained in:
Manus
2026-10-09 10:22:46 +00:00
parent 6d72547509
commit 16759b4ad5
6 changed files with 176 additions and 20 deletions

View File

@@ -1,4 +1,4 @@
import { eq, and, desc, sql, inArray, getTableColumns, count } from "drizzle-orm";
import { eq, and, desc, sql, inArray, getTableColumns, count, lt } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import {
InsertUser,
@@ -628,6 +628,25 @@ export async function deleteAllImportLogs(userId: number | null): Promise<void>
}
}
/** Compte les seuls journaux d'import qui seront concernés par une purge de rétention. */
export async function countImportLogsBefore(cutoff: Date): Promise<number> {
const db = await getDb();
if (!db) return 0;
const [result] = await db.select({ total: count() }).from(importLogs).where(lt(importLogs.importedAt, cutoff));
return Number(result?.total ?? 0);
}
/**
* Supprime uniquement les journaux d'import antérieurs à la borne donnée.
* Les factures, fichiers source et PDF ne sont jamais modifiés.
*/
export async function deleteImportLogsBefore(cutoff: Date): Promise<number> {
const db = await getDb();
if (!db) throw new Error("Database not available");
const result = await db.delete(importLogs).where(lt(importLogs.importedAt, cutoff));
return Number(result[0]?.affectedRows ?? 0);
}
// ============= LLM LOG OPERATIONS =============
export async function createLlmLog(data: InsertLlmLog): Promise<LlmLog> {

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import {
IMPORT_LOG_RETENTION_OPTIONS,
getImportLogRetentionCutoff,
isImportLogRetentionMonths,
} from "@shared/importLogRetention";
describe("rétention des journaux d'import", () => {
it("propose uniquement les périodes de purge autorisées", () => {
expect(IMPORT_LOG_RETENTION_OPTIONS).toEqual([3, 6, 12]);
expect(isImportLogRetentionMonths(3)).toBe(true);
expect(isImportLogRetentionMonths(6)).toBe(true);
expect(isImportLogRetentionMonths(12)).toBe(true);
expect(isImportLogRetentionMonths(9)).toBe(false);
});
it("calcule une date limite calendaire sans modifier la date de référence", () => {
const now = new Date("2026-10-09T12:00:00.000Z");
const cutoff = getImportLogRetentionCutoff(6, now);
expect(cutoff.toISOString()).toBe("2026-04-09T12:00:00.000Z");
expect(now.toISOString()).toBe("2026-10-09T12:00:00.000Z");
});
});

View File

@@ -4,6 +4,7 @@ import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapElig
import { requiresBapBeforeOriginalPdfExport } from "@shared/bapExportGuard";
import { calculateInvoiceQualityScore, withCalculatedInvoiceQuality } from "@shared/invoiceQuality";
import { normalizeImportLogPagination } from "@shared/importLogPagination";
import { getImportLogRetentionCutoff, isImportLogRetentionMonths } from "@shared/importLogRetention";
import { webPortalUrlSchema } from "./webPortalUrl";
interface Condition {
@@ -50,6 +51,8 @@ import {
getImportLogsByUser,
getImportLogsWithDetailsByUser,
deleteAllImportLogs,
countImportLogsBefore,
deleteImportLogsBefore,
getLlmLogsBySourceFile,
getLlmLogsByInvoice,
getImportSettingsByUser,
@@ -1748,6 +1751,34 @@ export const appRouter = router({
}
return { success: true };
}),
retentionPreview: protectedProcedure
.input(z.object({ months: z.number().int() }))
.query(async ({ ctx, input }) => {
if (ctx.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN", message: "Réservé aux administrateurs." });
}
if (!isImportLogRetentionMonths(input.months)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Durée de rétention invalide." });
}
const cutoff = getImportLogRetentionCutoff(input.months);
const count = await countImportLogsBefore(cutoff);
return { months: input.months, cutoff, count };
}),
purgeBeforeRetention: protectedProcedure
.input(z.object({ months: z.number().int() }))
.mutation(async ({ ctx, input }) => {
if (ctx.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN", message: "Réservé aux administrateurs." });
}
if (!isImportLogRetentionMonths(input.months)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Durée de rétention invalide." });
}
const cutoff = getImportLogRetentionCutoff(input.months);
const deletedCount = await deleteImportLogsBefore(cutoff);
return { months: input.months, cutoff, deletedCount };
}),
}),
// ============= LLM LOGS ROUTES =============