diff --git a/ManagerService.Tests/Controllers/SectionEventControllerTests.cs b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs index 6a9eb5e..fed04a5 100644 --- a/ManagerService.Tests/Controllers/SectionEventControllerTests.cs +++ b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs @@ -43,8 +43,7 @@ namespace ManagerService.Tests.Controllers ConfigurationId = "c1", Title = EmptyTranslations(), Description = EmptyTranslations(), - Programme = new List(), - ParcoursIds = new List() + Programme = new List() }); db.SaveChanges(); diff --git a/ManagerService.Tests/Helpers/ResourceStorageTests.cs b/ManagerService.Tests/Helpers/ResourceStorageTests.cs new file mode 100644 index 0000000..4ab5e33 --- /dev/null +++ b/ManagerService.Tests/Helpers/ResourceStorageTests.cs @@ -0,0 +1,62 @@ +using ManagerService.Data; +using ManagerService.Helpers; +using Xunit; + +namespace ManagerService.Tests.Helpers +{ + /// + /// Le calculateur est appelé par la création, le backfill et MigrationController. + /// Ces tests fixent la distinction qu'ils perdaient chacun de leur côté : un type + /// URL n'a pas de blob, donc ni chemin de stockage ni poids au quota. + /// + public class ResourceStorageTests + { + [Theory] + [InlineData(ResourceType.Image)] + [InlineData(ResourceType.Video)] + [InlineData(ResourceType.Audio)] + [InlineData(ResourceType.PDF)] + [InlineData(ResourceType.JSON)] + [InlineData(ResourceType.Word)] + [InlineData(ResourceType.PowerPoint)] + [InlineData(ResourceType.Text)] + public void Apply_BlobType_WritesPathAndSize(ResourceType type) + { + var resource = new Resource { Id = "res-1", InstanceId = "inst-1", Type = type }; + + ResourceStorage.Apply(resource, 4096); + + Assert.Equal("pictures/inst-1/res-1", resource.StoragePath); + Assert.Equal(4096, resource.SizeBytes); + } + + [Theory] + [InlineData(ResourceType.ImageUrl)] + [InlineData(ResourceType.VideoUrl)] + [InlineData(ResourceType.JSONUrl)] + public void Apply_UrlType_WritesNothing(ResourceType type) + { + var resource = new Resource { Id = "res-1", InstanceId = "inst-1", Type = type }; + + ResourceStorage.Apply(resource, 4096); + + Assert.Null(resource.StoragePath); + // Reste à 0 : un média hébergé ailleurs ne doit pas peser sur le quota. + Assert.Equal(0, resource.SizeBytes); + } + + [Fact] + public void PathFor_IsDeterministic_SoBackfillCanRebuildIt() + { + Assert.Equal( + ResourceStorage.PathFor(ResourceType.Image, "inst-1", "res-1"), + ResourceStorage.PathFor(ResourceType.Image, "inst-1", "res-1")); + } + + [Fact] + public void PathFor_UrlType_IsNull() + { + Assert.Null(ResourceStorage.PathFor(ResourceType.ImageUrl, "inst-1", "res-1")); + } + } +} diff --git a/ManagerService/Controllers/AuthenticationController.cs b/ManagerService/Controllers/AuthenticationController.cs index b3f0332..d55c004 100644 --- a/ManagerService/Controllers/AuthenticationController.cs +++ b/ManagerService/Controllers/AuthenticationController.cs @@ -60,11 +60,10 @@ namespace ManagerService.Service.Controllers { try { -#if DEBUG - email = "test@email.be"; - password = "kljqsdkljqsd"; // password = "kljqsdkljqsd"; // W/7aj4NB60i3YFKJq50pbw== -#endif - // Set user token ? + // Retiré le 2026-08-11 : un bloc `#if DEBUG` écrasait ici l'email et le + // mot de passe reçus par un compte de test, donc toute compilation en + // Debug authentifiait n'importe qui en tant que test@email.be. + // Set user token ? var user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Email.ToLower() == email.ToLower()); //var user = _UserDatabaseService.GetByEmail(email.ToLower()); diff --git a/ManagerService/Controllers/MigrationController.cs b/ManagerService/Controllers/MigrationController.cs index e1b4170..a22e4d0 100644 --- a/ManagerService/Controllers/MigrationController.cs +++ b/ManagerService/Controllers/MigrationController.cs @@ -466,7 +466,7 @@ namespace ManagerService.Controllers MapMapType = dto?.mapType, MapTypeMapbox = dto?.mapTypeMapbox, MapMapProvider = dto?.mapProvider, - MapResourceId = dto?.iconResourceId, + IconResourceId = dto?.iconResourceId, MapCenterLatitude = dto?.latitude, MapCenterLongitude = dto?.longitude, MapCategories = dto?.categories?.Select(c => new CategorieDTO diff --git a/ManagerService/Controllers/ResourceController.cs b/ManagerService/Controllers/ResourceController.cs index 6549a0e..735d5b3 100644 --- a/ManagerService/Controllers/ResourceController.cs +++ b/ManagerService/Controllers/ResourceController.cs @@ -260,14 +260,9 @@ namespace ManagerService.Controllers { file.CopyTo(ms); var fileBytes = ms.ToArray(); - if (resourceType == ResourceType.Image) + if (resourceType == ResourceType.Image && IsWatermarkEnabled(instanceId)) { - bool isFort = instanceId == "633ee379d9405f32f166f047"; // If fort saint heribert, TODO add watermark in configuration and model - - if(isFort) // TODO We need to know for which purpose (mobile or tablet) - { - fileBytes = ImageHelper.ResizeAndAddWatermark(fileBytes, isFort, MaxWidth, MaxHeight); - } + fileBytes = ImageHelper.ResizeAndAddWatermark(fileBytes, true, MaxWidth, MaxHeight); } stringResult = Convert.ToBase64String(fileBytes); } @@ -283,7 +278,7 @@ namespace ManagerService.Controllers resource.DateCreation = DateTime.Now.ToUniversalTime(); resource.InstanceId = instanceId; resource.Id = idService.GenerateHexId(); - resource.SizeBytes = file.Length; + ResourceStorage.Apply(resource, file.Length); _myInfoMateDbContext.Add(resource); _myInfoMateDbContext.SaveChanges(); @@ -339,15 +334,7 @@ namespace ManagerService.Controllers resource.InstanceId = newResource.instanceId; resource.Id = idService.GenerateHexId(); - // Les types URL pointent hors du bucket : ni chemin de stockage, ni poids à compter dans le quota. - bool hasBlob = resource.Type != ResourceType.ImageUrl - && resource.Type != ResourceType.VideoUrl - && resource.Type != ResourceType.JSONUrl; - if (hasBlob) - { - resource.StoragePath = $"pictures/{resource.InstanceId}/{resource.Id}"; - resource.SizeBytes = newResource.sizeBytes; - } + ResourceStorage.Apply(resource, newResource.sizeBytes); _myInfoMateDbContext.Add(resource); _myInfoMateDbContext.SaveChanges(); @@ -466,7 +453,7 @@ namespace ManagerService.Controllers switch (section) { case SectionMap map: - map.MapResourceId = map.MapResourceId == id ? null : map.MapResourceId; + map.IconResourceId = map.IconResourceId == id ? null : map.IconResourceId; List geoPoints = _myInfoMateDbContext.GeoPoints.Where(s => s.SectionMapId == section.Id).ToList(); foreach (var point in geoPoints) { @@ -716,5 +703,15 @@ namespace ManagerService.Controllers return new ObjectResult(ex.Message) { StatusCode = 500 }; } } + + /// + /// Le filigrane est un réglage d'instance depuis le 2026-08-11. Auparavant + /// un `instanceId == "633ee379…"` en dur ne l'activait que pour Fort Saint-Héribert. + /// + private bool IsWatermarkEnabled(string instanceId) => + _myInfoMateDbContext.Instances + .Where(i => i.Id == instanceId) + .Select(i => i.IsImageWatermark) + .FirstOrDefault(); } } diff --git a/ManagerService/DTOs/InstanceDTO.cs b/ManagerService/DTOs/InstanceDTO.cs index cb29d15..39c843d 100644 --- a/ManagerService/DTOs/InstanceDTO.cs +++ b/ManagerService/DTOs/InstanceDTO.cs @@ -16,6 +16,7 @@ namespace ManagerService.DTOs public bool? isVR { get; set; } public bool? isAssistant { get; set; } + public bool? isImageWatermark { get; set; } public string? guideName { get; set; } public string? guidePersonaPrompt { get; set; } diff --git a/ManagerService/DTOs/SubSection/SectionEventDTO.cs b/ManagerService/DTOs/SubSection/SectionEventDTO.cs index a9006ec..ee301ae 100644 --- a/ManagerService/DTOs/SubSection/SectionEventDTO.cs +++ b/ManagerService/DTOs/SubSection/SectionEventDTO.cs @@ -10,7 +10,6 @@ namespace Manager.DTOs public DateTime? StartDate { get; set; } public DateTime? EndDate { get; set; } public string? BaseSectionMapId { get; set; } - public List ParcoursIds { get; set; } public List GlobalMapAnnotations { get; set; } = new(); public List Programme { get; set; } = new(); } diff --git a/ManagerService/Data/Instance.cs b/ManagerService/Data/Instance.cs index 191af9f..c7901e3 100644 --- a/ManagerService/Data/Instance.cs +++ b/ManagerService/Data/Instance.cs @@ -36,6 +36,12 @@ namespace ManagerService.Data public bool IsAssistant { get; set; } + /// + /// Appose le filigrane du lieu sur les images téléversées. Remplace le + /// `instanceId == "633ee379…"` en dur de ResourceController.Create. + /// + public bool IsImageWatermark { get; set; } + /// Nom du guide affiché au visiteur, libre. Ex: "Léon". public string? GuideName { get; set; } @@ -118,6 +124,7 @@ namespace ManagerService.Data isWeb = IsWeb, isVR = IsVR, isAssistant = IsAssistant, + isImageWatermark = IsImageWatermark, guideName = GuideName, guidePersonaPrompt = GuidePersonaPrompt, guideVoiceId = GuideVoiceId, @@ -155,6 +162,7 @@ namespace ManagerService.Data IsWeb = instanceDTO.isWeb ?? false; IsVR = instanceDTO.isVR ?? false; IsAssistant = instanceDTO.isAssistant ?? false; + IsImageWatermark = instanceDTO.isImageWatermark ?? false; if (instanceDTO.guideName != null) GuideName = instanceDTO.guideName; if (instanceDTO.guidePersonaPrompt != null) diff --git a/ManagerService/Data/SubSection/SectionEvent.cs b/ManagerService/Data/SubSection/SectionEvent.cs index 5b2afa4..815e1ab 100644 --- a/ManagerService/Data/SubSection/SectionEvent.cs +++ b/ManagerService/Data/SubSection/SectionEvent.cs @@ -24,8 +24,6 @@ namespace ManagerService.Data.SubSection public SectionMap? BaseMap { get; set; } public List GlobalMapAnnotations { get; set; } = new(); public List Programme { get; set; } = new(); - [Column(TypeName = "jsonb")] - public List ParcoursIds { get; set; } = new(); // Liens vers GeoPoints spécifiques public override string GetEmbeddableText(string language) => JoinText(new[] diff --git a/ManagerService/Data/SubSection/SectionMap.cs b/ManagerService/Data/SubSection/SectionMap.cs index 1ac6126..73a8395 100644 --- a/ManagerService/Data/SubSection/SectionMap.cs +++ b/ManagerService/Data/SubSection/SectionMap.cs @@ -22,8 +22,8 @@ namespace ManagerService.Data.SubSection public MapTypeMapBox? MapTypeMapbox { get; set; } // Default = standard for MapBox public MapProvider? MapMapProvider { get; set; } // Default = Google public List MapPoints { get; set; } - public string MapResourceId { get; set; } - public Resource MapResource { get; set; } // Icon + public string IconResourceId { get; set; } + public Resource IconResource { get; set; } // Icon [Required] [Column(TypeName = "jsonb")] public List MapCategories { get; set; } @@ -42,7 +42,7 @@ namespace ManagerService.Data.SubSection public override IEnumerable GetReferencedResourceIds(string language = null) => BaseResourceIds() - .Concat(ResourceId(MapResourceId)) + .Concat(ResourceId(IconResourceId)) .Concat((MapCategories ?? new List()) .SelectMany(c => ResourceId(c.resourceDTO?.id))) .Concat((MapPoints ?? new List()) @@ -74,7 +74,7 @@ namespace ManagerService.Data.SubSection mapType = MapMapType, mapTypeMapbox = MapTypeMapbox, mapProvider = MapMapProvider, - iconResourceId = MapResourceId, + iconResourceId = IconResourceId, categories = MapCategories, centerLatitude = MapCenterLatitude, centerLongitude = MapCenterLongitude diff --git a/ManagerService/Helpers/ResourceStorage.cs b/ManagerService/Helpers/ResourceStorage.cs new file mode 100644 index 0000000..d071caf --- /dev/null +++ b/ManagerService/Helpers/ResourceStorage.cs @@ -0,0 +1,45 @@ +using ManagerService.Data; + +namespace ManagerService.Helpers +{ + /// + /// Seule source de vérité pour StoragePath et SizeBytes d'une Resource. + /// + /// Trois chemins écrivent ces colonnes : la création par téléversement et la + /// création par JSON (ResourceController), le backfill des lignes existantes, + /// et MigrationController (écart e de la bascule). Chacun calculant pour son + /// compte, ils divergeaient déjà : le chemin multipart renseignait SizeBytes + /// mais laissait StoragePath nul. + /// + public static class ResourceStorage + { + /// + /// Les types URL pointent hors du bucket : ni chemin de stockage, ni poids + /// à compter dans le quota. C'est la distinction que les appelants perdaient. + /// + public static bool HasBlob(ResourceType type) => + type != ResourceType.ImageUrl + && type != ResourceType.VideoUrl + && type != ResourceType.JSONUrl; + + /// + /// Chemin déterministe dans le bucket — reconstructible sans lire la ligne, + /// ce dont le backfill dépend. Null pour un type sans blob. + /// + public static string PathFor(ResourceType type, string instanceId, string resourceId) => + HasBlob(type) ? $"pictures/{instanceId}/{resourceId}" : null; + + /// + /// Renseigne les deux colonnes de stockage. Ne touche à rien pour un type URL, + /// dont SizeBytes doit rester à 0 pour ne pas peser sur le quota. + /// + public static void Apply(Resource resource, long sizeBytes) + { + if (!HasBlob(resource.Type)) + return; + + resource.StoragePath = PathFor(resource.Type, resource.InstanceId, resource.Id); + resource.SizeBytes = sizeBytes; + } + } +} diff --git a/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs new file mode 100644 index 0000000..478df12 --- /dev/null +++ b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.Designer.cs @@ -0,0 +1,1883 @@ +// +using System; +using System.Collections.Generic; +using Manager.DTOs; +using ManagerService.DTOs; +using ManagerService.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NetTopologySuite.Geometries; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260811130738_LotB_FreezeSchema")] + partial class LotB_FreezeSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis"); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ManagerService.Data.ApiKey", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("KeyHash") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("ManagerService.Data.AppConfigurationLink", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ApplicationInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("text"); + + b.Property("GridColSpan") + .HasColumnType("integer"); + + b.Property("GridRowSpan") + .HasColumnType("integer"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("RoundedValue") + .HasColumnType("integer"); + + b.Property("ScreenPercentageSectionsMainPage") + .HasColumnType("integer"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationInstanceId"); + + b.HasIndex("ConfigurationId"); + + b.HasIndex("DeviceId"); + + b.ToTable("AppConfigurationLinks"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("MainImageId") + .HasColumnType("text"); + + b.Property("MainImageUrl") + .HasColumnType("text"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionEventId"); + + b.ToTable("ApplicationInstances"); + }); + + modelBuilder.Entity("ManagerService.Data.AuditLog", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Action") + .HasColumnType("text"); + + b.Property("EntityId") + .HasColumnType("text"); + + b.Property("EntityType") + .HasColumnType("text"); + + b.Property("InstanceId") + .HasColumnType("text"); + + b.Property("NewValues") + .HasColumnType("text"); + + b.Property("OldValues") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageId") + .HasColumnType("text"); + + b.Property("ImageSource") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsOffline") + .HasColumnType("boolean"); + + b.Property("IsQRCode") + .HasColumnType("boolean"); + + b.Property("IsSearchNumber") + .HasColumnType("boolean"); + + b.Property("IsSearchText") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LoaderImageId") + .HasColumnType("text"); + + b.Property("LoaderImageUrl") + .HasColumnType("text"); + + b.Property("PrimaryColor") + .HasColumnType("text"); + + b.Property("SecondaryColor") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Configurations"); + }); + + modelBuilder.Entity("ManagerService.Data.ContentEmbedding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChunkIndex") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("ContentId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("integer"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(768)"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("PageNumber") + .HasColumnType("integer"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Embedding"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Embedding"), "hnsw"); + NpgsqlIndexBuilderExtensions.HasOperators(b.HasIndex("Embedding"), new[] { "vector_cosine_ops" }); + + b.HasIndex("InstanceId", "ContentType"); + + b.HasIndex("ContentType", "ContentId", "ChunkIndex") + .IsUnique(); + + b.ToTable("ContentEmbeddings"); + }); + + modelBuilder.Entity("ManagerService.Data.Device", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppVersion") + .HasColumnType("text"); + + b.Property("BatteryLevel") + .HasColumnType("text"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Connected") + .HasColumnType("boolean"); + + b.Property("ConnectionLevel") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Identifier") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IpAddressETH") + .HasColumnType("text"); + + b.Property("IpAddressWLAN") + .HasColumnType("text"); + + b.Property("LastBatteryLevel") + .HasColumnType("timestamp with time zone"); + + b.Property("LastConnectionLevel") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ConfigurationId"); + + b.HasIndex("InstanceId"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiTokensPerMonth") + .HasColumnType("bigint"); + + b.Property("AiTokensThisMonth") + .HasColumnType("bigint"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("BillingAddress") + .HasColumnType("text"); + + b.Property("BillingCountry") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("GuideFallbackMessages") + .HasColumnType("jsonb"); + + b.Property("GuideName") + .HasColumnType("text"); + + b.Property("GuidePersonaPrompt") + .HasColumnType("text"); + + b.Property("GuideVoiceId") + .HasColumnType("text"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsImageWatermark") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .HasColumnType("boolean"); + + b.Property("IsTrialActive") + .HasColumnType("boolean"); + + b.Property("IsVR") + .HasColumnType("boolean"); + + b.Property("IsWeb") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PinCode") + .HasColumnType("text"); + + b.Property("PublicApiKey") + .HasColumnType("text"); + + b.Property("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.Property("StripeCustomerId") + .HasColumnType("text"); + + b.Property("StripeSubscriptionId") + .HasColumnType("text"); + + b.Property("SubscriptionPlanId") + .HasColumnType("text"); + + b.Property("TrialAiTokensUsed") + .HasColumnType("bigint"); + + b.Property("TrialCheckInEmailSent") + .HasColumnType("boolean"); + + b.Property("TrialEndsAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrialLastDayEmailSent") + .HasColumnType("boolean"); + + b.Property("TrialReminderEmailSent") + .HasColumnType("boolean"); + + b.Property("VatNumber") + .HasColumnType("text"); + + b.Property("VatRate") + .HasColumnType("numeric"); + + b.Property("WebSlug") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SubscriptionPlanId"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("ManagerService.Data.PushNotification", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("HangfireJobId") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ScheduledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.Property("Topic") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("PushNotifications"); + }); + + modelBuilder.Entity("ManagerService.Data.Resource", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiChunkCount") + .HasColumnType("integer"); + + b.Property("AiIndexMessage") + .HasColumnType("text"); + + b.Property("AiIndexStatus") + .HasColumnType("integer"); + + b.Property("AiIndexedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("IncludeInAiKnowledge") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StoragePath") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Resources"); + }); + + modelBuilder.Entity("ManagerService.Data.Section", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("BeaconId") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ImageId") + .HasColumnType("text"); + + b.Property("ImageSource") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsBeacon") + .HasColumnType("boolean"); + + b.Property("IsSubSection") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("Latitude") + .HasColumnType("text"); + + b.Property("Longitude") + .HasColumnType("text"); + + b.Property("MeterZoneGPS") + .HasColumnType("integer"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("ParentId") + .HasColumnType("text"); + + b.Property("SectionMenuId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionMenuId"); + + b.ToTable("Sections"); + + b.HasDiscriminator().HasValue("Base"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.EventAgenda", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("jsonb"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone"); + + b.Property("DateFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("DateTo") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IdVideoYoutube") + .HasColumnType("text"); + + b.Property("IsSynced") + .HasColumnType("boolean"); + + b.Property("Label") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("ResourceId") + .HasColumnType("text"); + + b.Property("SectionAgendaId") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SyncedImageUrl") + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("text"); + + b.Property("VideoLink") + .HasColumnType("text"); + + b.Property("VideoResourceId") + .HasColumnType("text"); + + b.Property("Website") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionAgendaId"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("VideoResourceId"); + + b.ToTable("EventAgendas"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GeoPoint", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CategorieId") + .HasColumnType("integer"); + + b.Property("Contents") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Description") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Email") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("ImageResourceId") + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("PolyColor") + .HasColumnType("text"); + + b.Property("Prices") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Schedules") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Site") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GeoPoints"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("EstimatedDurationMinutes") + .HasColumnType("integer"); + + b.Property("GameMessageDebut") + .HasColumnType("jsonb"); + + b.Property("GameMessageFin") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("ImageResourceId") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsGameMode") + .HasColumnType("boolean"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionParcoursId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("ImageResourceId"); + + b.HasIndex("InstanceId"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionParcoursId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AudioIds") + .HasColumnType("jsonb"); + + b.Property("Contents") + .HasColumnType("jsonb"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsGeoTriggered") + .HasColumnType("boolean"); + + b.Property("IsStepTimer") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("TimerExpiredMessage") + .HasColumnType("jsonb"); + + b.Property("TimerSeconds") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.ToTable("GuidedSteps"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.QuizQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GuidedStepId") + .HasColumnType("text"); + + b.Property("IsSlidingPuzzle") + .HasColumnType("boolean"); + + b.Property>("Label") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PuzzleCols") + .HasColumnType("integer"); + + b.Property("PuzzleImageId") + .HasColumnType("text"); + + b.Property("PuzzleRows") + .HasColumnType("integer"); + + b.Property("ResourceId") + .HasColumnType("text"); + + b.Property>("Responses") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("SectionQuizId") + .HasColumnType("text"); + + b.Property("ValidationQuestionType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GuidedStepId"); + + b.HasIndex("PuzzleImageId"); + + b.HasIndex("ResourceId"); + + b.HasIndex("SectionQuizId"); + + b.ToTable("QuizQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+MapAnnotation", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GeometryType") + .HasColumnType("integer"); + + b.Property("Icon") + .HasColumnType("text"); + + b.Property("IconResourceId") + .HasColumnType("text"); + + b.Property>("Label") + .HasColumnType("jsonb"); + + b.Property("PolyColor") + .HasColumnType("text"); + + b.Property("ProgrammeBlockId") + .HasColumnType("text"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property>("Type") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("IconResourceId"); + + b.HasIndex("ProgrammeBlockId"); + + b.HasIndex("SectionEventId"); + + b.ToTable("MapAnnotations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property>("Description") + .HasColumnType("jsonb"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property>("Title") + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.ToTable("ProgrammeBlocks"); + }); + + modelBuilder.Entity("ManagerService.Data.SubscriptionPlan", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiTokensPerMonth") + .HasColumnType("bigint"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.ToTable("SubscriptionPlans"); + + b.HasData( + new + { + Id = "plan-starter", + AiTokensPerMonth = 0L, + HasAdvancedStats = false, + HasStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiTokensPerMonth = 5000000L, + HasAdvancedStats = false, + HasStats = true, + Name = "Standard", + StatsHistoryDays = 395, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiTokensPerMonth = 20000000L, + HasAdvancedStats = true, + HasStats = true, + Name = "Premium", + StatsHistoryDays = 395, + StorageQuotaBytes = 53687091200L + }, + new + { + Id = "plan-essentiel", + AiTokensPerMonth = 500000L, + HasAdvancedStats = false, + HasStats = true, + Name = "Essentiel", + StatsHistoryDays = 395, + StorageQuotaBytes = 1073741824L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("DateUpdate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FirstName") + .HasColumnType("text"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("PasswordTokenExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordTokenHash") + .HasColumnType("text"); + + b.Property("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("ManagerService.Data.VisitEvent", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("DurationSeconds") + .HasColumnType("integer"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Language") + .HasColumnType("text"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("SectionId") + .HasColumnType("text"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Timestamp"); + + b.ToTable("VisitEvents"); + }); + + modelBuilder.Entity("ManagerService.Data.VisitorQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AppType") + .HasColumnType("integer"); + + b.Property("CitedContentIds") + .HasColumnType("jsonb"); + + b.Property("ConfigurationId") + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HasAnswer") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .HasColumnType("text"); + + b.Property("IsVoice") + .HasColumnType("boolean"); + + b.Property("Language") + .HasMaxLength(5) + .HasColumnType("character varying(5)"); + + b.Property("Question") + .HasColumnType("text"); + + b.Property("Reply") + .HasColumnType("text"); + + b.Property("ThemeId") + .HasColumnType("text"); + + b.Property("TokensUsed") + .HasColumnType("bigint"); + + b.Property("TopScore") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("InstanceId", "CreatedAt"); + + b.ToTable("VisitorQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionAgenda", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("AgendaMapProvider") + .HasColumnType("integer"); + + b.Property>("AgendaResourceIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("IsOnlineAgenda") + .HasColumnType("boolean"); + + b.HasDiscriminator().HasValue("Agenda"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionArticle", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("ArticleAudioIds") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("ArticleContent") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("ArticleContents") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("ArticleIsContentTop") + .HasColumnType("boolean"); + + b.Property("ArticleIsReadAudioAuto") + .HasColumnType("boolean"); + + b.HasDiscriminator().HasValue("Article"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("BaseSectionMapId") + .HasColumnType("text"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.HasIndex("BaseSectionMapId"); + + b.HasDiscriminator().HasValue("Event"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionGame", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("GameMessageDebut") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("GameMessageFin") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("GamePuzzleCols") + .HasColumnType("integer"); + + b.Property("GamePuzzleImageId") + .HasColumnType("text"); + + b.Property("GamePuzzleRows") + .HasColumnType("integer"); + + b.Property("GameType") + .HasColumnType("integer"); + + b.HasIndex("GamePuzzleImageId"); + + b.HasDiscriminator().HasValue("Game"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("IconResourceId") + .HasColumnType("text"); + + b.Property("IsListViewEnabled") + .HasColumnType("boolean"); + + b.Property>("MapCategories") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property("MapCenterLatitude") + .HasColumnType("text"); + + b.Property("MapCenterLongitude") + .HasColumnType("text"); + + b.Property("MapMapProvider") + .HasColumnType("integer"); + + b.Property("MapMapType") + .HasColumnType("integer"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("IconResourceId"); + + b.HasDiscriminator().HasValue("Map"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMenu", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.HasDiscriminator().HasValue("Menu"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("BaseSectionMapId") + .HasColumnType("text"); + + b.Property("ShowMap") + .HasColumnType("boolean"); + + b.HasIndex("BaseSectionMapId"); + + b.ToTable("Sections", t => + { + t.Property("BaseSectionMapId") + .HasColumnName("SectionParcours_BaseSectionMapId"); + }); + + b.HasDiscriminator().HasValue("Parcours"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionPdf", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("PDFOrderedTranslationAndResources") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("PDF"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionQuiz", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("QuizBadLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizGoodLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizGreatLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.Property>("QuizMediumLevel") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("Quiz"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionSlider", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property>("SliderContents") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasDiscriminator().HasValue("Slider"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionVideo", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("VideoSource") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Video"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionWeather", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("WeatherCity") + .HasColumnType("text"); + + b.Property("WeatherResult") + .HasColumnType("text"); + + b.Property("WeatherUpdatedDate") + .HasColumnType("timestamp with time zone"); + + b.HasDiscriminator().HasValue("Weather"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionWeb", b => + { + b.HasBaseType("ManagerService.Data.Section"); + + b.Property("WebSource") + .IsRequired() + .HasColumnType("text"); + + b.HasDiscriminator().HasValue("Web"); + }); + + modelBuilder.Entity("ManagerService.Data.ApiKey", b => + { + b.HasOne("ManagerService.Data.Instance", "Instance") + .WithMany() + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Instance"); + }); + + modelBuilder.Entity("ManagerService.Data.AppConfigurationLink", b => + { + b.HasOne("ManagerService.Data.ApplicationInstance", "ApplicationInstance") + .WithMany("Configurations") + .HasForeignKey("ApplicationInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.Configuration", "Configuration") + .WithMany() + .HasForeignKey("ConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId"); + + b.Navigation("ApplicationInstance"); + + b.Navigation("Configuration"); + + b.Navigation("Device"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.Navigation("SectionEvent"); + }); + + modelBuilder.Entity("ManagerService.Data.Device", b => + { + b.HasOne("ManagerService.Data.Configuration", "Configuration") + .WithMany() + .HasForeignKey("ConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Configuration"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.HasOne("ManagerService.Data.SubscriptionPlan", "SubscriptionPlan") + .WithMany() + .HasForeignKey("SubscriptionPlanId"); + + b.Navigation("SubscriptionPlan"); + }); + + modelBuilder.Entity("ManagerService.Data.Section", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMenu", null) + .WithMany("MenuSections") + .HasForeignKey("SectionMenuId"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.EventAgenda", b => + { + b.HasOne("ManagerService.Data.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionAgenda", "SectionAgenda") + .WithMany("EventAgendas") + .HasForeignKey("SectionAgendaId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.Resource", "VideoResource") + .WithMany() + .HasForeignKey("VideoResourceId"); + + b.Navigation("Resource"); + + b.Navigation("SectionAgenda"); + + b.Navigation("SectionEvent"); + + b.Navigation("VideoResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GeoPoint", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany("MapPoints") + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.HasOne("ManagerService.Data.Resource", "ImageResource") + .WithMany() + .HasForeignKey("ImageResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionParcours", "SectionParcours") + .WithMany("GuidedPaths") + .HasForeignKey("SectionParcoursId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("ImageResource"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionParcours"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuidedPath"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.QuizQuestion", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedStep", "GuidedStep") + .WithMany("QuizQuestions") + .HasForeignKey("GuidedStepId"); + + b.HasOne("ManagerService.Data.Resource", "PuzzleImage") + .WithMany() + .HasForeignKey("PuzzleImageId"); + + b.HasOne("ManagerService.Data.Resource", "Resource") + .WithMany() + .HasForeignKey("ResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionQuiz", "SectionQuiz") + .WithMany("QuizQuestions") + .HasForeignKey("SectionQuizId"); + + b.Navigation("GuidedStep"); + + b.Navigation("PuzzleImage"); + + b.Navigation("Resource"); + + b.Navigation("SectionQuiz"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+MapAnnotation", b => + { + b.HasOne("ManagerService.Data.Resource", "IconResource") + .WithMany() + .HasForeignKey("IconResourceId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", null) + .WithMany("MapAnnotations") + .HasForeignKey("ProgrammeBlockId"); + + b.HasOne("ManagerService.Data.SubSection.SectionEvent", null) + .WithMany("GlobalMapAnnotations") + .HasForeignKey("SectionEventId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("IconResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionEvent", null) + .WithMany("Programme") + .HasForeignKey("SectionEventId"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMap", "BaseMap") + .WithMany() + .HasForeignKey("BaseSectionMapId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("BaseMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionGame", b => + { + b.HasOne("ManagerService.Data.Resource", "GamePuzzleImage") + .WithMany() + .HasForeignKey("GamePuzzleImageId"); + + b.Navigation("GamePuzzleImage"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.HasOne("ManagerService.Data.Resource", "IconResource") + .WithMany() + .HasForeignKey("IconResourceId"); + + b.Navigation("IconResource"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.HasOne("ManagerService.Data.SubSection.SectionMap", "BaseMap") + .WithMany() + .HasForeignKey("BaseSectionMapId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("BaseMap"); + }); + + modelBuilder.Entity("ManagerService.Data.ApplicationInstance", b => + { + b.Navigation("Configurations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedPath", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Navigation("QuizQuestions"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent+ProgrammeBlock", b => + { + b.Navigation("MapAnnotations"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionAgenda", b => + { + b.Navigation("EventAgendas"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionEvent", b => + { + b.Navigation("GlobalMapAnnotations"); + + b.Navigation("Programme"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => + { + b.Navigation("MapPoints"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionMenu", b => + { + b.Navigation("MenuSections"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => + { + b.Navigation("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs new file mode 100644 index 0000000..b4cb7f8 --- /dev/null +++ b/ManagerService/Migrations/20260811130738_LotB_FreezeSchema.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class LotB_FreezeSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Resources_MapResourceId", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "ParcoursIds", + table: "Sections"); + + migrationBuilder.RenameColumn( + name: "MapResourceId", + table: "Sections", + newName: "IconResourceId"); + + migrationBuilder.RenameIndex( + name: "IX_Sections_MapResourceId", + table: "Sections", + newName: "IX_Sections_IconResourceId"); + + migrationBuilder.AddColumn( + name: "IsImageWatermark", + table: "Instances", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Resources_IconResourceId", + table: "Sections", + column: "IconResourceId", + principalTable: "Resources", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Resources_IconResourceId", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "IsImageWatermark", + table: "Instances"); + + migrationBuilder.RenameColumn( + name: "IconResourceId", + table: "Sections", + newName: "MapResourceId"); + + migrationBuilder.RenameIndex( + name: "IX_Sections_IconResourceId", + table: "Sections", + newName: "IX_Sections_MapResourceId"); + + migrationBuilder.AddColumn>( + name: "ParcoursIds", + table: "Sections", + type: "jsonb", + nullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Resources_MapResourceId", + table: "Sections", + column: "MapResourceId", + principalTable: "Resources", + principalColumn: "Id"); + } + } +} diff --git a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs index 766971e..f0b91c6 100644 --- a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs +++ b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs @@ -465,6 +465,9 @@ namespace ManagerService.Migrations b.Property("IsAssistant") .HasColumnType("boolean"); + b.Property("IsImageWatermark") + .HasColumnType("boolean"); + b.Property("IsMobile") .HasColumnType("boolean"); @@ -1398,9 +1401,6 @@ namespace ManagerService.Migrations b.Property("EndDate") .HasColumnType("timestamp with time zone"); - b.Property>("ParcoursIds") - .HasColumnType("jsonb"); - b.Property("StartDate") .HasColumnType("timestamp with time zone"); @@ -1442,6 +1442,9 @@ namespace ManagerService.Migrations { b.HasBaseType("ManagerService.Data.Section"); + b.Property("IconResourceId") + .HasColumnType("text"); + b.Property("IsListViewEnabled") .HasColumnType("boolean"); @@ -1461,16 +1464,13 @@ namespace ManagerService.Migrations b.Property("MapMapType") .HasColumnType("integer"); - b.Property("MapResourceId") - .HasColumnType("text"); - b.Property("MapTypeMapbox") .HasColumnType("integer"); b.Property("MapZoom") .HasColumnType("integer"); - b.HasIndex("MapResourceId"); + b.HasIndex("IconResourceId"); b.HasDiscriminator().HasValue("Map"); }); @@ -1806,11 +1806,11 @@ namespace ManagerService.Migrations modelBuilder.Entity("ManagerService.Data.SubSection.SectionMap", b => { - b.HasOne("ManagerService.Data.Resource", "MapResource") + b.HasOne("ManagerService.Data.Resource", "IconResource") .WithMany() - .HasForeignKey("MapResourceId"); + .HasForeignKey("IconResourceId"); - b.Navigation("MapResource"); + b.Navigation("IconResource"); }); modelBuilder.Entity("ManagerService.Data.SubSection.SectionParcours", b => diff --git a/ManagerService/Services/SectionFactory.cs b/ManagerService/Services/SectionFactory.cs index 5c15378..d9ddd59 100644 --- a/ManagerService/Services/SectionFactory.cs +++ b/ManagerService/Services/SectionFactory.cs @@ -31,8 +31,7 @@ namespace ManagerService.Services }, SectionType.Event => new SectionEvent { - Programme = new List(), - ParcoursIds = new List() + Programme = new List() }, SectionType.Map => new SectionMap { @@ -196,7 +195,6 @@ namespace ManagerService.Services Type = dto.type, StartDate = sectionEventDTO.StartDate?.ToUniversalTime(), EndDate = sectionEventDTO.EndDate?.ToUniversalTime(), - ParcoursIds = sectionEventDTO.ParcoursIds, BaseSectionMapId = sectionEventDTO.BaseSectionMapId, //Programmes = // TODO specific }, @@ -224,7 +222,7 @@ namespace ManagerService.Services MapMapType = mapDTO.mapType, MapTypeMapbox = mapDTO.mapTypeMapbox, MapMapProvider = mapDTO.mapProvider, - MapResourceId = mapDTO.iconResourceId, + IconResourceId = mapDTO.iconResourceId, MapCenterLatitude = mapDTO.centerLatitude, MapCenterLongitude = mapDTO.centerLongitude, MapCategories = mapDTO.categories, @@ -526,7 +524,6 @@ namespace ManagerService.Services type = sectionEvent.Type, StartDate = sectionEvent.StartDate?.Year > 1000 ? sectionEvent.StartDate : null, EndDate = sectionEvent.EndDate?.Year > 1000 ? sectionEvent.EndDate : null, - ParcoursIds = sectionEvent.ParcoursIds, BaseSectionMapId = sectionEvent.BaseSectionMapId, GlobalMapAnnotations = sectionEvent.GlobalMapAnnotations?.Select(ma => ma.ToDTO()).ToList() ?? new(), // Programme TODO specific @@ -556,7 +553,7 @@ namespace ManagerService.Services mapType = map.MapMapType, mapTypeMapbox = map.MapTypeMapbox, mapProvider = map.MapMapProvider, - iconResourceId = map.MapResourceId, + iconResourceId = map.IconResourceId, centerLatitude = map.MapCenterLatitude, centerLongitude = map.MapCenterLongitude, categories = map.MapCategories, diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs index 4591124..511d06b 100644 --- a/ManagerService/Startup.cs +++ b/ManagerService/Startup.cs @@ -243,10 +243,17 @@ namespace ManagerService services.AddScoped(); services.AddDbContext((serviceProvider, options) => - options.UseNpgsql(dataSource, o => o.UseNetTopologySuite().UseVector()) - .AddInterceptors(serviceProvider.GetRequiredService()) - .EnableSensitiveDataLogging() - .LogTo(Console.WriteLine, LogLevel.Information) + { + options.UseNpgsql(dataSource, o => o.UseNetTopologySuite().UseVector()) + .AddInterceptors(serviceProvider.GetRequiredService()); +#if DEBUG + // Écrit les valeurs des paramètres dans les logs : mots de passe hashés, + // clés API, données de visiteurs. Jamais en production — le Dockerfile + // publie en `-c Release`, donc ce bloc n'y est pas compilé. + options.EnableSensitiveDataLogging() + .LogTo(Console.WriteLine, LogLevel.Information); +#endif + } ); services.AddHealthChecks()