diff --git a/ManagerService.Tests/Services/AuditLogPurgePostgresTests.cs b/ManagerService.Tests/Services/AuditLogPurgePostgresTests.cs
new file mode 100644
index 0000000..371941c
--- /dev/null
+++ b/ManagerService.Tests/Services/AuditLogPurgePostgresTests.cs
@@ -0,0 +1,68 @@
+using ManagerService.Data;
+using ManagerService.Services;
+using ManagerService.Tests.Infrastructure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace ManagerService.Tests.Services
+{
+ ///
+ /// La purge s'appuie sur ExecuteDeleteAsync, que le provider InMemory ne sait pas
+ /// traduire — un test là-bas ne dirait rien de ce qui se passe en production. Ceux-ci
+ /// s'exécutent contre un vrai PostgreSQL, et se sautent proprement sans démon Docker.
+ ///
+ [Collection(PostgresCollection.Name)]
+ public class AuditLogPurgePostgresTests
+ {
+ private readonly PostgresFixture _postgres;
+
+ public AuditLogPurgePostgresTests(PostgresFixture postgres)
+ {
+ _postgres = postgres;
+ }
+
+ private static AuditLogPurgeService BuildService(MyInfoMateDbContext db, int retentionDays)
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Audit:RetentionDays"] = retentionDays.ToString()
+ })
+ .Build();
+
+ return new AuditLogPurgeService(db, configuration,
+ NullLogger.Instance);
+ }
+
+ private static AuditLog Entry(string id, DateTime timestamp) => new AuditLog
+ {
+ Id = id,
+ EntityType = "User",
+ EntityId = "u1",
+ Action = "Update",
+ UserId = "u1",
+ Timestamp = timestamp
+ };
+
+ [SkippableFact]
+ public async Task PurgeAsync_DeletesOnlyBeyondTheWindow()
+ {
+ Skip.IfNot(_postgres.Available, _postgres.SkipReason);
+
+ using var db = _postgres.CreateMigratedContext("audit_purge");
+ db.AuditLogs.AddRange(
+ Entry("vieux", DateTime.UtcNow.AddDays(-400)),
+ Entry("recent", DateTime.UtcNow.AddDays(-30)));
+ db.SaveChanges();
+
+ await BuildService(db, AuditLogPurgeService.DefaultRetentionDays).PurgeAsync();
+
+ Assert.Equal("recent", Assert.Single(db.AuditLogs).Id);
+ }
+ }
+}
diff --git a/ManagerService.Tests/Services/AuditLogPurgeServiceTests.cs b/ManagerService.Tests/Services/AuditLogPurgeServiceTests.cs
new file mode 100644
index 0000000..1da793e
--- /dev/null
+++ b/ManagerService.Tests/Services/AuditLogPurgeServiceTests.cs
@@ -0,0 +1,71 @@
+using ManagerService.Data;
+using ManagerService.Services;
+using ManagerService.Tests.Infrastructure;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace ManagerService.Tests.Services
+{
+ public class AuditLogPurgeServiceTests
+ {
+ private static AuditLogPurgeService BuildService(MyInfoMateDbContext db, int? retentionDays)
+ {
+ var settings = new Dictionary();
+ if (retentionDays != null)
+ settings["Audit:RetentionDays"] = retentionDays.ToString();
+
+ var configuration = new ConfigurationBuilder().AddInMemoryCollection(settings).Build();
+ return new AuditLogPurgeService(db, configuration,
+ NullLogger.Instance);
+ }
+
+ private static AuditLog Entry(string id, DateTime timestamp) => new AuditLog
+ {
+ Id = id,
+ EntityType = "User",
+ EntityId = "u1",
+ Action = "Update",
+ UserId = "u1",
+ Timestamp = timestamp
+ };
+
+ ///
+ /// Le verrou : sans configuration, rien n'est supprimé. C'est une suppression
+ /// définitive et le pg_dump quotidien n'est pas en place — un job de purge qui
+ /// partirait tout seul est exactement ce qu'on ne veut pas.
+ ///
+ [Fact]
+ public async Task PurgeAsync_WithoutConfiguration_DeletesNothing()
+ {
+ using var db = DbContextFactory.Create();
+ db.AuditLogs.Add(Entry("a1", DateTime.UtcNow.AddYears(-5)));
+ db.SaveChanges();
+
+ await BuildService(db, null).PurgeAsync();
+
+ Assert.Single(db.AuditLogs);
+ }
+
+ // ⚠️ La suppression elle-même se teste contre un vrai PostgreSQL, pas ici :
+ // `ExecuteDeleteAsync` n'est pas traduisible par le provider InMemory. Écrire la
+ // purge en chargeant les lignes puis `RemoveRange` la rendrait testable ici au prix
+ // de charger un an de journal en mémoire — dégrader le code de production pour
+ // satisfaire un provider de test. Voir AuditLogPurgePostgresTests.
+
+ ///
+ /// 12 mois, pas 13 : les 13 mois des statistiques existent pour comparer une saison à
+ /// la précédente, les recopier ici serait du mimétisme. Ce test fige l'écart voulu.
+ ///
+ [Fact]
+ public void DefaultRetention_IsTwelveMonths_NotThirteen()
+ {
+ Assert.Equal(365, AuditLogPurgeService.DefaultRetentionDays);
+ Assert.NotEqual(MyInfoMateDbContext.StatsRetentionDays, AuditLogPurgeService.DefaultRetentionDays);
+ }
+ }
+}
diff --git a/ManagerService/Services/AuditLogPurgeService.cs b/ManagerService/Services/AuditLogPurgeService.cs
new file mode 100644
index 0000000..69464af
--- /dev/null
+++ b/ManagerService/Services/AuditLogPurgeService.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using ManagerService.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace ManagerService.Services
+{
+ ///
+ /// Purge le journal d'audit au-delà de .
+ ///
+ /// ⚠️ **C'est un sujet RGPD, et rien d'autre.** `AuditLog` porte un `UserId`, et les
+ /// valeurs avant/après d'une modification de `User` contiennent e-mail, prénom et nom —
+ /// conservés sans limite jusqu'ici, alors que `VisitEvent` purge à 13 mois et
+ /// `VisitorQuestion` à 90 jours. **Ce n'est pas un sujet de volume** : depuis que le flot
+ /// machine de `WeatherSyncService` est coupé (2026-08-12), la table ne croît qu'au rythme
+ /// des éditions humaines.
+ ///
+ /// **12 mois, uniforme.** Pas 13 : les 13 mois des statistiques existent pour comparer une
+ /// saison à la précédente, et recopier ce chiffre ici serait du mimétisme. ⛔ Écarté aussi :
+ /// garder les suppressions plus longtemps que les créations et modifications — ça double
+ /// les règles pour un cas qu'une sauvegarde couvre déjà.
+ ///
+ /// ⚠️ **Inerte tant que Audit:RetentionDays n'est pas défini**, exactement comme
+ /// et pour la même raison : c'est une suppression
+ /// définitive, et le pg_dump quotidien n'est toujours pas en place. Supprimer des lignes
+ /// d'audit sans restauration fine est la pire combinaison possible.
+ ///
+ /// ⛔ Ne pas s'aligner sur , qui tourne sans
+ /// condition : ses 90 jours sont un engagement écrit dans les CGU, pas un réglage
+ /// d'exploitation. Ici, aucun contrat ne fixe la durée.
+ ///
+ public class AuditLogPurgeService
+ {
+ /// Durée recommandée. Voir le lot J du plan V1.
+ public const int DefaultRetentionDays = 365;
+
+ private readonly MyInfoMateDbContext _dbContext;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+
+ public AuditLogPurgeService(
+ MyInfoMateDbContext dbContext,
+ IConfiguration configuration,
+ ILogger logger)
+ {
+ _dbContext = dbContext;
+ _configuration = configuration;
+ _logger = logger;
+ }
+
+ public async Task PurgeAsync()
+ {
+ var retentionDays = _configuration.GetValue("Audit:RetentionDays");
+ if (retentionDays <= 0)
+ {
+ _logger.LogInformation(
+ "Purge du journal d'audit ignorée : Audit:RetentionDays non défini. " +
+ "Valeur attendue une fois le pg_dump en place : {Expected}.",
+ DefaultRetentionDays);
+ return;
+ }
+
+ var cutoff = DateTime.UtcNow.AddDays(-retentionDays);
+ var deleted = await _dbContext.AuditLogs
+ .Where(a => a.Timestamp < cutoff)
+ .ExecuteDeleteAsync();
+
+ _logger.LogInformation(
+ "Purge du journal d'audit : {Deleted} entrée(s) antérieure(s) au {Cutoff:yyyy-MM-dd} supprimée(s).",
+ deleted, cutoff);
+ }
+ }
+}
diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs
index 17e11a5..f74dbdb 100644
--- a/ManagerService/Startup.cs
+++ b/ManagerService/Startup.cs
@@ -217,6 +217,7 @@ namespace ManagerService
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
var connectionString = Configuration.GetConnectionString("PostgresConnection");
@@ -393,6 +394,13 @@ namespace ManagerService
s => s.PurgeAsync(),
"30 3 * * *");
+ // Ne supprime rien tant qu'Audit:RetentionDays n'est pas défini — même verrou que
+ // les VisitEvent, et pour la même raison : pas de pg_dump, pas de suppression.
+ RecurringJob.AddOrUpdate(
+ "audit-log-purge",
+ s => s.PurgeAsync(),
+ "45 3 * * *");
+
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();