using Manager.DTOs; using ManagerService.Data.SubSection; using ManagerService.DTOs; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using static ManagerService.Data.SubSection.SectionEvent; namespace ManagerService.Data { public class MyInfoMateDbContext : DbContext { private readonly IHttpContextAccessor _httpContextAccessor; public MyInfoMateDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) : base(options) { _httpContextAccessor = httpContextAccessor; } public DbSet Instances { get; set; } public DbSet SubscriptionPlans { get; set; } public DbSet Configurations { get; set; } public DbSet
Sections { get; set; } public DbSet Devices { get; set; } public DbSet Resources { get; set; } public DbSet Users { get; set; } public DbSet ApplicationInstances { get; set; } public DbSet AppConfigurationLinks { get; set; } // MAP public DbSet GeoPoints { get; set; } // QUIZ public DbSet QuizQuestions { get; set; } public DbSet GuidedPaths { get; set; } public DbSet GuidedSteps { get; set; } // Events public DbSet ProgrammeBlocks { get; set; } public DbSet MapAnnotations { get; set; } // Agenda public DbSet EventAgendas { get; set; } // Statistics public DbSet VisitEvents { get; set; } // API Keys public DbSet ApiKeys { get; set; } // Push Notifications public DbSet PushNotifications { get; set; } // Audit public DbSet AuditLogs { get; set; } public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var auditEntries = BuildAuditEntries(); var result = await base.SaveChangesAsync(cancellationToken); if (auditEntries.Any()) await base.SaveChangesAsync(cancellationToken); return result; } private static readonly HashSet AuditedTypes = new() { typeof(Section), typeof(Resource), typeof(Configuration), typeof(Device), typeof(User), typeof(Instance) }; private List BuildAuditEntries() { var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier); var entries = new List(); foreach (var entry in ChangeTracker.Entries() .Where(e => AuditedTypes.Contains(e.Entity.GetType()) && e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)) { var action = entry.State switch { EntityState.Added => "Create", EntityState.Modified => "Update", EntityState.Deleted => "Delete", _ => null }; var entityId = entry.Properties .FirstOrDefault(p => p.Metadata.IsPrimaryKey())?.CurrentValue?.ToString(); var instanceId = entry.Properties .FirstOrDefault(p => p.Metadata.Name == "InstanceId")?.CurrentValue?.ToString(); string? oldValues = null; string? newValues = null; if (entry.State == EntityState.Modified) { oldValues = JsonSerializer.Serialize( entry.Properties.Where(p => p.IsModified) .ToDictionary(p => p.Metadata.Name, p => p.OriginalValue)); newValues = JsonSerializer.Serialize( entry.Properties.Where(p => p.IsModified) .ToDictionary(p => p.Metadata.Name, p => p.CurrentValue)); } else if (entry.State == EntityState.Added) { newValues = JsonSerializer.Serialize( entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue)); } var log = new AuditLog { EntityType = entry.Entity.GetType().Name, EntityId = entityId ?? "", Action = action!, UserId = userId, InstanceId = instanceId, OldValues = oldValues, NewValues = newValues }; AuditLogs.Add(log); entries.Add(log); } return entries; } protected override void OnModelCreating(ModelBuilder modelBuilder) { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; base.OnModelCreating(modelBuilder); // Les types suivants sont utilisés uniquement comme valeurs JSONB, pas comme entités DB. // Le provider InMemory les découvre par convention et échoue à la validation // si aucune clé primaire n'est définie. On les exclut explicitement. modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Ignore(); modelBuilder.Entity() .Property(s => s.Title) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity
() .Property("Discriminator") .HasMaxLength(50); modelBuilder.Entity
() .HasDiscriminator("Discriminator") .HasValue
("Base") .HasValue("Agenda") .HasValue("Article") .HasValue("Event") .HasValue("Map") .HasValue("Menu") .HasValue("PDF") .HasValue("Game") .HasValue("Quiz") .HasValue("Slider") .HasValue("Video") .HasValue("Weather") .HasValue("Web"); /*modelBuilder.Entity(entity => { entity.Property(e => e.Geometry).HasColumnType("geometry"); }); modelBuilder.Entity(entity => { entity.Property(e => e.Geometry).HasColumnType("geometry"); });*/ modelBuilder.Entity
() .Property(s => s.Title) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity
() .Property(s => s.Description) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Title) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Description) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Contents) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Schedules) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Prices) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Phone) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Email) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Site) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); // Configurations JSON pour GuidedPath modelBuilder.Entity() .Property(gp => gp.Title) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(gp => gp.Description) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); // Configurations JSON pour GuidedStep modelBuilder.Entity() .Property(gs => gs.Title) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(gs => gs.Description) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(gp => gp.TimerExpiredMessage) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Label) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Description) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize>(v, options)); modelBuilder.Entity() .Property(s => s.Address) .HasColumnType("jsonb") .HasConversion( v => JsonSerializer.Serialize(v, options), v => JsonSerializer.Deserialize(v, options)); // SectionEvent: link to base SectionMap modelBuilder.Entity() .HasOne(se => se.BaseMap) .WithMany() .HasForeignKey(se => se.BaseSectionMapId) .IsRequired(false) .OnDelete(DeleteBehavior.SetNull); // MapAnnotation: global event-level annotations linked directly to SectionEvent modelBuilder.Entity() .HasOne() .WithMany(se => se.GlobalMapAnnotations) .HasForeignKey(ma => ma.SectionEventId) .IsRequired(false) .OnDelete(DeleteBehavior.Cascade); // Ces colonnes ont un defaultValue dans la migration, ce qui amène EF Core à les traiter // comme ValueGeneratedOnAdd (read-only après insert). On force ValueGeneratedNever. modelBuilder.Entity() .Property(i => i.StorageQuotaBytes).ValueGeneratedNever(); modelBuilder.Entity() .Property(i => i.AiRequestsPerMonth).ValueGeneratedNever(); modelBuilder.Entity() .Property(i => i.HasStats).ValueGeneratedNever(); modelBuilder.Entity() .Property(i => i.StatsHistoryDays).ValueGeneratedNever(); modelBuilder.Entity() .Property(i => i.HasAdvancedStats).ValueGeneratedNever(); // Seed : plans d'abonnement par défaut modelBuilder.Entity().HasData( new SubscriptionPlan { Id = "plan-starter", Name = "Starter", StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB AiRequestsPerMonth = 0, HasStats = false, StatsHistoryDays = 30, HasAdvancedStats = false, }, new SubscriptionPlan { Id = "plan-standard", Name = "Standard", StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB AiRequestsPerMonth = 500, HasStats = true, StatsHistoryDays = 30, HasAdvancedStats = false, }, new SubscriptionPlan { Id = "plan-premium", Name = "Premium", StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB AiRequestsPerMonth = 2000, HasStats = true, StatsHistoryDays = 0, HasAdvancedStats = true, } ); } } }