From e0ff0eeba612d8e81234bb94beac43690a1b6beb Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Fri, 10 Apr 2026 16:28:33 +0200 Subject: [PATCH] Quota update in controller, audit log (serilog) + ai translator + Unit tests ! (to be tested) + migration update --- .gitignore | 3 + .../Controllers/AiControllerTests.cs | 176 ++ .../Controllers/ApiKeyControllerTests.cs | 87 + .../ApplicationInstanceControllerTests.cs | 139 ++ .../AuthenticationControllerTests.cs | 80 + .../ConfigurationControllerTests.cs | 104 ++ .../Controllers/DeviceControllerTests.cs | 136 ++ .../Controllers/InstanceControllerTests.cs | 247 +++ .../Controllers/ResourceControllerTests.cs | 144 ++ .../SectionAgendaControllerTests.cs | 195 ++ .../Controllers/SectionControllerTests.cs | 125 ++ .../SectionEventControllerTests.cs | 186 ++ .../Controllers/SectionMapControllerTests.cs | 142 ++ .../Controllers/SectionQuizControllerTests.cs | 195 ++ .../Controllers/StatsControllerTests.cs | 179 ++ .../SubscriptionPlanControllerTests.cs | 169 ++ .../Controllers/UserControllerTests.cs | 185 ++ .../Infrastructure/DbContextFactory.cs | 17 + .../Infrastructure/FakeMongoConfig.cs | 22 + .../Infrastructure/FakeUser.cs | 61 + .../ManagerService.Tests.csproj | 33 + ManagerService.Tests/nuget.config | 7 + ManagerService.sln | 6 + ManagerService/AssemblyInfo.cs | 3 + ManagerService/Controllers/AiController.cs | 38 +- ManagerService/Controllers/AuditController.cs | 64 + .../Controllers/InstanceController.cs | 32 +- .../Controllers/MigrationController.cs | 34 +- .../Controllers/ResourceController.cs | 21 +- ManagerService/Controllers/StatsController.cs | 57 +- .../Controllers/SubscriptionPlanController.cs | 1 + ManagerService/DTOs/AiChatDTO.cs | 12 + ManagerService/DTOs/InstanceDTO.cs | 21 +- ManagerService/DTOs/SubscriptionPlanDTO.cs | 3 + ManagerService/Data/AuditLog.cs | 17 + ManagerService/Data/Device.cs | 4 + ManagerService/Data/Instance.cs | 39 +- ManagerService/Data/MyInfoMateDbContext.cs | 130 +- ManagerService/Data/SubscriptionPlan.cs | 17 +- ManagerService/ManagerService.csproj | 3 + .../20260401152545_SeedSubscriptionPlans.cs | 6 +- ...StatsHistoryToSubscriptionPlan.Designer.cs | 1557 ++++++++++++++++ ...22319_AddStatsHistoryToSubscriptionPlan.cs | 104 ++ ...122549_FixPremiumStatsSettings.Designer.cs | 1557 ++++++++++++++++ .../20260410122549_FixPremiumStatsSettings.cs | 32 + ...23835_AddQuotaFieldsToInstance.Designer.cs | 1578 +++++++++++++++++ ...20260410123835_AddQuotaFieldsToInstance.cs | 119 ++ ...ixInstanceQuotaColumnsWritable.Designer.cs | 1578 +++++++++++++++++ ...0124619_FixInstanceQuotaColumnsWritable.cs | 22 + .../MyInfoMateDbContextModelSnapshot.cs | 77 +- ManagerService/Program.cs | 28 +- ManagerService/Services/AssistantService.cs | 36 +- ManagerService/Services/IAssistantService.cs | 11 + ManagerService/Startup.cs | 11 +- ManagerService/appsettings.Development.json | 4 +- ManagerService/appsettings.PreProduction.json | 18 +- ManagerService/appsettings.json | 17 +- docker-compose-myinfomate.yml | 210 +++ docker-compose.yml | 46 +- 59 files changed, 10041 insertions(+), 104 deletions(-) create mode 100644 ManagerService.Tests/Controllers/AiControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/ApiKeyControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/ApplicationInstanceControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/AuthenticationControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/ConfigurationControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/DeviceControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/InstanceControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/ResourceControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SectionControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SectionEventControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SectionMapControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SectionQuizControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/StatsControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/SubscriptionPlanControllerTests.cs create mode 100644 ManagerService.Tests/Controllers/UserControllerTests.cs create mode 100644 ManagerService.Tests/Infrastructure/DbContextFactory.cs create mode 100644 ManagerService.Tests/Infrastructure/FakeMongoConfig.cs create mode 100644 ManagerService.Tests/Infrastructure/FakeUser.cs create mode 100644 ManagerService.Tests/ManagerService.Tests.csproj create mode 100644 ManagerService.Tests/nuget.config create mode 100644 ManagerService/AssemblyInfo.cs create mode 100644 ManagerService/Controllers/AuditController.cs create mode 100644 ManagerService/Data/AuditLog.cs create mode 100644 ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs create mode 100644 ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.cs create mode 100644 ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs create mode 100644 ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.cs create mode 100644 ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs create mode 100644 ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.cs create mode 100644 ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs create mode 100644 ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.cs create mode 100644 ManagerService/Services/IAssistantService.cs create mode 100644 docker-compose-myinfomate.yml diff --git a/.gitignore b/.gitignore index 217edaa..ce02ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ ManagerService/obj # Firebase service account key (sensitive — never commit) *firebase-adminsdk*.json + +# MongoDB export data (données sensibles — ne jamais committer) +migration-data/ diff --git a/ManagerService.Tests/Controllers/AiControllerTests.cs b/ManagerService.Tests/Controllers/AiControllerTests.cs new file mode 100644 index 0000000..8098c4b --- /dev/null +++ b/ManagerService.Tests/Controllers/AiControllerTests.cs @@ -0,0 +1,176 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Services; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class AiControllerTests + { + private static readonly AiChatResponse FakeResponse = new AiChatResponse { Reply = "OK" }; + + private AiController BuildController(MyInfoMateDbContext db, Mock? mockService = null) + { + mockService ??= new Mock(); + mockService + .Setup(s => s.ChatAsync(It.IsAny())) + .ReturnsAsync(FakeResponse); + + return new AiController(mockService.Object, db, NullLogger.Instance); + } + + private static AiChatRequest MakeRequest(string instanceId, AppType appType = AppType.Tablet) => + new AiChatRequest { InstanceId = instanceId, AppType = appType, Message = "Bonjour" }; + + // ── FORBID CASES ───────────────────────────────────────────────────── + + [Fact] + public async Task Chat_InstanceNotFound_ReturnsForbid() + { + using var db = DbContextFactory.Create(); + + var result = await BuildController(db).Chat(MakeRequest("unknown")); + + Assert.IsType(result); + } + + [Fact] + public async Task Chat_InstanceAssistantDisabled_ReturnsForbid() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = false, DateCreation = DateTime.UtcNow + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true, + Languages = new List() + }); + db.SaveChanges(); + + var result = await BuildController(db).Chat(MakeRequest("i1")); + + Assert.IsType(result); + } + + [Fact] + public async Task Chat_AppInstanceAssistantDisabled_ReturnsForbid() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = false, + Languages = new List() + }); + db.SaveChanges(); + + var result = await BuildController(db).Chat(MakeRequest("i1")); + + Assert.IsType(result); + } + + [Fact] + public async Task Chat_NoAppInstance_ReturnsForbid() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow + }); + db.SaveChanges(); + + var result = await BuildController(db).Chat(MakeRequest("i1")); + + Assert.IsType(result); + } + + // ── QUOTA COUNTER ──────────────────────────────────────────────────── + + [Fact] + public async Task Chat_FirstRequestOfMonth_ResetsCounterAndSets1() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow, + AiRequestsThisMonth = 99, AiUsageMonthKey = "2020-01" + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true, + Languages = new List() + }); + db.SaveChanges(); + + await BuildController(db).Chat(MakeRequest("i1")); + + var inst = db.Instances.First(); + Assert.Equal(1, inst.AiRequestsThisMonth); + Assert.Equal(DateTime.UtcNow.ToString("yyyy-MM"), inst.AiUsageMonthKey); + } + + [Fact] + public async Task Chat_SameMonth_IncrementsCounter() + { + using var db = DbContextFactory.Create(); + var monthKey = DateTime.UtcNow.ToString("yyyy-MM"); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow, + AiRequestsThisMonth = 3, AiUsageMonthKey = monthKey + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true, + Languages = new List() + }); + db.SaveChanges(); + + await BuildController(db).Chat(MakeRequest("i1")); + + Assert.Equal(4, db.Instances.First().AiRequestsThisMonth); + } + + // ── NOMINAL ────────────────────────────────────────────────────────── + + [Fact] + public async Task Chat_Success_CallsAssistantServiceAndReturns200() + { + using var db = DbContextFactory.Create(); + var mockService = new Mock(); + mockService.Setup(s => s.ChatAsync(It.IsAny())).ReturnsAsync(FakeResponse); + + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", IsAssistant = true, DateCreation = DateTime.UtcNow, + AiUsageMonthKey = DateTime.UtcNow.ToString("yyyy-MM") + }); + db.ApplicationInstances.Add(new ApplicationInstance + { + Id = "ai1", InstanceId = "i1", AppType = AppType.Tablet, IsAssistant = true, + Languages = new List() + }); + db.SaveChanges(); + + var result = await BuildController(db, mockService).Chat(MakeRequest("i1")); + + var ok = Assert.IsType(result); + Assert.Equal(FakeResponse, ok.Value); + mockService.Verify(s => s.ChatAsync(It.IsAny()), Times.Once); + } + } +} diff --git a/ManagerService.Tests/Controllers/ApiKeyControllerTests.cs b/ManagerService.Tests/Controllers/ApiKeyControllerTests.cs new file mode 100644 index 0000000..92e93e0 --- /dev/null +++ b/ManagerService.Tests/Controllers/ApiKeyControllerTests.cs @@ -0,0 +1,87 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.Services; +using static ManagerService.Services.ApiKeyDatabaseService; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class ApiKeyControllerTests + { + private static ApiKeyController BuildController(MyInfoMateDbContext db, string instanceId = "inst-test") + { + var service = new ApiKeyDatabaseService(db); + var controller = new ApiKeyController(service); + FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", instanceId)); + return controller; + } + + [Fact] + public async Task GetApiKeys_ReturnsKeysForInstance() + { + using var db = DbContextFactory.Create(); + db.ApiKeys.AddRange( + new ApiKey { Id = "k1", InstanceId = "inst-test", Name = "Key1", KeyHash = "h1", IsActive = true, AppType = ApiKeyAppType.VisitApp }, + new ApiKey { Id = "k2", InstanceId = "other-inst", Name = "Key2", KeyHash = "h2", IsActive = true, AppType = ApiKeyAppType.VisitApp } + ); + db.SaveChanges(); + + var result = await BuildController(db).GetApiKeys(); + + var ok = Assert.IsType(result); + var keys = Assert.IsAssignableFrom>(ok.Value); + Assert.Single(keys); + } + + [Fact] + public async Task CreateApiKey_ValidRequest_ReturnsPlainKey() + { + using var db = DbContextFactory.Create(); + + var result = await BuildController(db).CreateApiKey(new CreateApiKeyRequest + { + Name = "My Key", + AppType = ApiKeyAppType.VisitApp + }); + + var ok = Assert.IsType(result); + Assert.NotNull(ok.Value); + // La clé plain text doit commencer par "ak_" + var keyProp = ok.Value!.GetType().GetProperty("key"); + Assert.NotNull(keyProp); + var plainKey = keyProp!.GetValue(ok.Value) as string; + Assert.StartsWith("ak_", plainKey); + + // La clé est stockée hachée, pas en clair + Assert.Equal(1, db.ApiKeys.Count()); + Assert.Null(db.ApiKeys.First().Key); + } + + [Fact] + public async Task RevokeApiKey_ExistingKey_Returns204() + { + using var db = DbContextFactory.Create(); + db.ApiKeys.Add(new ApiKey { Id = "k1", InstanceId = "inst-test", Name = "Key1", KeyHash = "h1", IsActive = true, AppType = ApiKeyAppType.VisitApp }); + db.SaveChanges(); + + var result = await BuildController(db).RevokeApiKey("k1"); + + Assert.IsType(result); + Assert.Equal(0, db.ApiKeys.Count(k => k.IsActive)); + } + + [Fact] + public async Task RevokeApiKey_UnknownKey_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = await BuildController(db).RevokeApiKey("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/ApplicationInstanceControllerTests.cs b/ManagerService.Tests/Controllers/ApplicationInstanceControllerTests.cs new file mode 100644 index 0000000..8df9753 --- /dev/null +++ b/ManagerService.Tests/Controllers/ApplicationInstanceControllerTests.cs @@ -0,0 +1,139 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Helpers; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class ApplicationInstanceControllerTests + { + private static ApplicationInstanceController BuildController(MyInfoMateDbContext db) + { + var cfg = FakeMongoConfig.Create(); + var controller = new ApplicationInstanceController( + NullLogger.Instance, + new InstanceDatabaseService(cfg), + new UserDatabaseService(cfg), + new ProfileLogic(NullLogger.Instance), + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", "inst-test")); + return controller; + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_ValidDto_Persists() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Create(new ApplicationInstanceDTO + { + instanceId = "inst-test", + appType = AppType.Tablet + }); + + Assert.IsType(result); + Assert.Equal(1, db.ApplicationInstances.Count()); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Update_ExistingAppInstance_UpdatesAppType() + { + using var db = DbContextFactory.Create(); + db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet }); + db.SaveChanges(); + + var result = BuildController(db).Update(new ApplicationInstanceDTO + { + id = "ai-1", + instanceId = "inst-test", + appType = AppType.Web + }); + + Assert.IsType(result); + Assert.Equal(AppType.Web, db.ApplicationInstances.First().AppType); + } + + [Fact] + public void Update_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Update(new ApplicationInstanceDTO { id = "unknown" }); + + Assert.IsType(result); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingAppInstance_Returns202() + { + using var db = DbContextFactory.Create(); + db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet }); + db.SaveChanges(); + + var result = BuildController(db).Delete("ai-1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.ApplicationInstances.Count()); + } + + // ── CONFIG LINK ────────────────────────────────────────────────────── + + [Fact] + public void AddConfigLink_DuplicatePrevented_Returns409() + { + using var db = DbContextFactory.Create(); + db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet, Configurations = new List() }); + db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", Title = new List() }); + db.AppConfigurationLinks.Add(new AppConfigurationLink { Id = "lnk-1", ApplicationInstanceId = "ai-1", ConfigurationId = "c1" }); + db.SaveChanges(); + + var result = BuildController(db).AddConfigurationToApplicationInstance("ai-1", new AppConfigurationLinkDTO + { + applicationInstanceId = "ai-1", + configurationId = "c1" + }); + + Assert.IsType(result); + } + + [Fact] + public void UpdateApplicationLinkOrder_ReordersCorrectly() + { + using var db = DbContextFactory.Create(); + db.Configurations.AddRange( + new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C1", Title = new List() }, + new Configuration { Id = "c2", InstanceId = "inst-test", Label = "C2", Title = new List() } + ); + db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet }); + db.AppConfigurationLinks.AddRange( + new AppConfigurationLink { Id = "lnk-1", ApplicationInstanceId = "ai-1", ConfigurationId = "c1", Order = 0 }, + new AppConfigurationLink { Id = "lnk-2", ApplicationInstanceId = "ai-1", ConfigurationId = "c2", Order = 1 } + ); + db.SaveChanges(); + + var result = BuildController(db).UpdateApplicationLinkOrder(new List + { + new AppConfigurationLinkDTO { id = "lnk-1", order = 1, configurationId = "c1" }, + new AppConfigurationLinkDTO { id = "lnk-2", order = 0, configurationId = "c2" } + }); + + Assert.IsType(result); + Assert.Equal(1, db.AppConfigurationLinks.First(l => l.Id == "lnk-1").Order); + Assert.Equal(0, db.AppConfigurationLinks.First(l => l.Id == "lnk-2").Order); + } + } +} diff --git a/ManagerService.Tests/Controllers/AuthenticationControllerTests.cs b/ManagerService.Tests/Controllers/AuthenticationControllerTests.cs new file mode 100644 index 0000000..f085ce5 --- /dev/null +++ b/ManagerService.Tests/Controllers/AuthenticationControllerTests.cs @@ -0,0 +1,80 @@ +using Manager.Interfaces.Models; +using Manager.Services; +using ManagerService.Data; +using ManagerService.Helpers; +using ManagerService.Service.Controllers; +using ManagerService.Service.Services; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class AuthenticationControllerTests + { + private static AuthenticationController BuildController(MyInfoMateDbContext db) + { + var settings = Options.Create(new TokensSettings + { + Secret = "test-secret-key-32-chars-minimum!!", + AccessTokenExpiration = 30 + }); + var profileLogic = new ProfileLogic(NullLogger.Instance); + var tokensService = new TokensService( + NullLogger.Instance, + settings, + profileLogic, + db); + + return new AuthenticationController( + NullLogger.Instance, + tokensService, + db); + } + + // Note: en mode DEBUG, email est toujours surchargé en "test@email.be" + // et password en "kljqsdkljqsd". + + [Fact] + public void Authenticate_UserNotFound_ReturnsProblem() + { + using var db = DbContextFactory.Create(); + // Aucun utilisateur "test@email.be" en base + + var result = BuildController(db).AuthenticateWithJson( + new ManagerService.DTOs.LoginDTO { email = "anyone@test.be", password = "any" }); + + // KeyNotFoundException → catch(Exception) → Problem (500) + var obj = Assert.IsType(result); + Assert.Equal(500, obj.StatusCode); + } + + [Fact] + public void Authenticate_WrongPassword_Returns401() + { + using var db = DbContextFactory.Create(); + // Mot de passe incorrect en base (hash invalide → PasswordUtils.Compare lance une exception + // qui n'est pas UnauthorizedAccessException → retourne Problem 500). + // On met un utilisateur avec un hash valide pour un autre mot de passe. + var profileLogic = new ProfileLogic(NullLogger.Instance); + db.Users.Add(new User + { + Id = "u1", + Email = "test@email.be", + Password = profileLogic.HashPassword("differentpassword"), + LastName = "Test", + Token = "t1", + InstanceId = "inst-test" + }); + db.SaveChanges(); + + var result = BuildController(db).AuthenticateWithJson( + new ManagerService.DTOs.LoginDTO { email = "test@email.be", password = "kljqsdkljqsd" }); + + // UnauthorizedAccessException → Unauthorized (401) + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/ConfigurationControllerTests.cs b/ManagerService.Tests/Controllers/ConfigurationControllerTests.cs new file mode 100644 index 0000000..b96ef6b --- /dev/null +++ b/ManagerService.Tests/Controllers/ConfigurationControllerTests.cs @@ -0,0 +1,104 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class ConfigurationControllerTests + { + private static IConfiguration BuildConfig() => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017", + ["OpenWeatherApiKey"] = "" + }) + .Build(); + + private static ConfigurationController BuildController(MyInfoMateDbContext db) => + new ConfigurationController(BuildConfig(), NullLogger.Instance, db); + + // ── GET ────────────────────────────────────────────────────────────── + + [Fact] + public void Get_FiltersToInstance() + { + using var db = DbContextFactory.Create(); + db.Configurations.AddRange( + new Configuration { Id = "c1", InstanceId = "inst-a", Label = "A", Title = new List() }, + new Configuration { Id = "c2", InstanceId = "inst-b", Label = "B", Title = new List() } + ); + db.SaveChanges(); + + var result = BuildController(db).Get("inst-a"); + + var ok = Assert.IsType(result); + var list = Assert.IsAssignableFrom(ok.Value); + Assert.Single(list.Cast()); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_ValidDto_Persists() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Create(new ConfigurationDTO + { + instanceId = "inst-a", + label = "My Config", + languages = new List { "FR" } + }); + + Assert.IsType(result); + Assert.Equal(1, db.Configurations.Count()); + Assert.Equal("My Config", db.Configurations.First().Label); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Update_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Update(new ConfigurationDTO { id = "unknown", label = "X" }); + + Assert.IsType(result); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingConfig_Returns202() + { + using var db = DbContextFactory.Create(); + db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-a", Label = "A", Title = new List() }); + db.SaveChanges(); + + var result = BuildController(db).Delete("c1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.Configurations.Count()); + } + + [Fact] + public void Delete_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/DeviceControllerTests.cs b/ManagerService.Tests/Controllers/DeviceControllerTests.cs new file mode 100644 index 0000000..1926228 --- /dev/null +++ b/ManagerService.Tests/Controllers/DeviceControllerTests.cs @@ -0,0 +1,136 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class DeviceControllerTests + { + private static DeviceController BuildController(MyInfoMateDbContext db) + { + var cfg = FakeMongoConfig.Create(); + var controller = new DeviceController( + NullLogger.Instance, + new DeviceDatabaseService(cfg), + new ConfigurationDatabaseService(cfg), + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.instanceadmin", "inst-test")); + return controller; + } + + private static void SeedPrerequisites(MyInfoMateDbContext db) + { + db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List() }); + db.ApplicationInstances.Add(new ApplicationInstance { Id = "ai-1", InstanceId = "inst-test", AppType = AppType.Tablet }); + db.SaveChanges(); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_NewIdentifier_CreatesDevice() + { + using var db = DbContextFactory.Create(); + SeedPrerequisites(db); + + var result = BuildController(db).Create(new DeviceDetailDTO + { + identifier = "device-abc", + instanceId = "inst-test", + configurationId = "conf-1", + name = "Tablet 1" + }); + + Assert.IsType(result); + Assert.Equal(1, db.Devices.Count()); + Assert.Equal("Tablet 1", db.Devices.First().Name); + } + + [Fact] + public void Create_ExistingIdentifier_UpdatesDeviceNoDuplicate() + { + using var db = DbContextFactory.Create(); + SeedPrerequisites(db); + db.Devices.Add(new Device { Id = "d1", Identifier = "device-abc", InstanceId = "inst-test", ConfigurationId = "conf-1" }); + db.SaveChanges(); + + var result = BuildController(db).Create(new DeviceDetailDTO + { + identifier = "device-abc", + instanceId = "inst-test", + configurationId = "conf-1", + name = "Updated Name" + }); + + Assert.IsType(result); + Assert.Equal(1, db.Devices.Count()); // pas de doublon + Assert.Equal("Updated Name", db.Devices.First().Name); + } + + [Fact] + public void Create_NewDevice_AutoCreatesAppConfigurationLink() + { + using var db = DbContextFactory.Create(); + SeedPrerequisites(db); + + BuildController(db).Create(new DeviceDetailDTO + { + identifier = "device-abc", + instanceId = "inst-test", + configurationId = "conf-1", + name = "Tablet 1" + }); + + Assert.Equal(1, db.AppConfigurationLinks.Count()); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Update_ExistingDevice_UpdatesName() + { + using var db = DbContextFactory.Create(); + db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List() }); + db.Devices.Add(new Device { Id = "d1", Identifier = "dev-1", InstanceId = "inst-test", Name = "Old Name", ConfigurationId = "conf-1" }); + db.SaveChanges(); + + var result = BuildController(db).Update(new DeviceDetailDTO { id = "d1", name = "New Name", instanceId = "inst-test", identifier = "dev-1" }); + + Assert.IsType(result); + Assert.Equal("New Name", db.Devices.First().Name); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingDevice_Returns202() + { + using var db = DbContextFactory.Create(); + db.Configurations.Add(new Configuration { Id = "conf-1", InstanceId = "inst-test", Label = "Conf", Title = new System.Collections.Generic.List() }); + db.Devices.Add(new Device { Id = "d1", Identifier = "dev-1", InstanceId = "inst-test", ConfigurationId = "conf-1" }); + db.SaveChanges(); + + var result = BuildController(db).Delete("d1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.Devices.Count()); + } + + [Fact] + public void Delete_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/InstanceControllerTests.cs b/ManagerService.Tests/Controllers/InstanceControllerTests.cs new file mode 100644 index 0000000..f07609a --- /dev/null +++ b/ManagerService.Tests/Controllers/InstanceControllerTests.cs @@ -0,0 +1,247 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Helpers; +using ManagerService.Services; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class InstanceControllerTests + { + private InstanceController BuildController(MyInfoMateDbContext db) + { + var cfg = FakeMongoConfig.Create(); + var instanceService = new InstanceDatabaseService(cfg); + var userService = new UserDatabaseService(cfg); + var profileLogic = new ProfileLogic(NullLogger.Instance); + var apiKeyService = new ApiKeyDatabaseService(db); + + return new InstanceController( + NullLogger.Instance, + instanceService, + userService, + profileLogic, + db, + apiKeyService); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void CreateInstance_DuplicateName_Returns409() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow }); + db.SaveChanges(); + + var result = BuildController(db).CreateInstance(new InstanceDTO { name = "Musée" }); + + Assert.IsType(result); + } + + [Fact] + public void CreateInstance_ValidDto_PersistsAndReturnsDto() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).CreateInstance(new InstanceDTO { name = "Nouveau" }); + + Assert.IsType(result); + Assert.Equal(1, db.Instances.Count()); + } + + [Fact] + public void CreateInstance_NullDto_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).CreateInstance(null!); + + Assert.IsType(result); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Updateinstance_ClearSubscriptionPlan_SetsNull() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" }); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", SubscriptionPlanId = "p1", DateCreation = DateTime.UtcNow }); + db.SaveChanges(); + + // subscriptionPlanId = "" → doit effacer le plan + var result = BuildController(db).Updateinstance(new InstanceDTO { id = "i1", subscriptionPlanId = "" }); + + Assert.IsType(result); + Assert.Null(db.Instances.First().SubscriptionPlanId); + } + + [Fact] + public void Updateinstance_SetSubscriptionPlan_Updates() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" }); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow }); + db.SaveChanges(); + + var result = BuildController(db).Updateinstance(new InstanceDTO { id = "i1", subscriptionPlanId = "p1" }); + + Assert.IsType(result); + Assert.Equal("p1", db.Instances.First().SubscriptionPlanId); + } + + [Fact] + public void Updateinstance_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Updateinstance(new InstanceDTO { id = "unknown", name = "X" }); + + Assert.IsType(result); + } + + // ── GET QUOTA ──────────────────────────────────────────────────────── + + [Fact] + public void GetQuota_UnknownInstance_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetQuota("unknown"); + + Assert.IsType(result); + } + + [Fact] + public void GetQuota_NoSubscriptionPlan_ReturnsZeroQuotas() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow }); + db.SaveChanges(); + + var result = BuildController(db).GetQuota("i1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal(0, dto.storageQuotaBytes); + Assert.Equal(0, dto.aiRequestsPerMonth); + } + + [Fact] + public void GetQuota_WithPlan_ReturnsQuotaFromPlan() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan + { + Id = "p1", Name = "Standard", + StorageQuotaBytes = 10_000_000_000L, + AiRequestsPerMonth = 100 + }); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", SubscriptionPlanId = "p1", DateCreation = DateTime.UtcNow + }); + db.SaveChanges(); + + var result = BuildController(db).GetQuota("i1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal(10_000_000_000L, dto.storageQuotaBytes); + Assert.Equal(100, dto.aiRequestsPerMonth); + } + + [Fact] + public void GetQuota_SumsResourceSizeBytes() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow }); + db.Resources.AddRange( + new Resource { Id = "r1", InstanceId = "i1", Label = "img1", Type = ResourceType.Image, SizeBytes = 1000 }, + new Resource { Id = "r2", InstanceId = "i1", Label = "img2", Type = ResourceType.Image, SizeBytes = 2000 }, + new Resource { Id = "r3", InstanceId = "other", Label = "img3", Type = ResourceType.Image, SizeBytes = 9999 } + ); + db.SaveChanges(); + + var result = BuildController(db).GetQuota("i1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal(3000, dto.storageUsedBytes); + } + + [Fact] + public void GetQuota_AiUsage_CurrentMonth_ReturnsCount() + { + using var db = DbContextFactory.Create(); + var monthKey = DateTime.UtcNow.ToString("yyyy-MM"); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow, + AiRequestsThisMonth = 7, AiUsageMonthKey = monthKey + }); + db.SaveChanges(); + + var result = BuildController(db).GetQuota("i1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal(7, dto.aiRequestsUsed); + } + + [Fact] + public void GetQuota_AiUsage_PreviousMonth_ReturnsZero() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance + { + Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow, + AiRequestsThisMonth = 7, AiUsageMonthKey = "2020-01" + }); + db.SaveChanges(); + + var result = BuildController(db).GetQuota("i1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal(0, dto.aiRequestsUsed); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void DeleteInstance_CascadesUsers() + { + using var db = DbContextFactory.Create(); + db.Instances.Add(new Instance { Id = "i1", Name = "Musée", DateCreation = DateTime.UtcNow }); + db.Users.AddRange( + new User { Id = "u1", Email = "a@test.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "i1" }, + new User { Id = "u2", Email = "b@test.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "i1" } + ); + db.SaveChanges(); + + var result = BuildController(db).DeleteInstance("i1"); + + var status = Assert.IsType(result); + Assert.Equal(202, status.StatusCode); + Assert.Equal(0, db.Instances.Count()); + Assert.Equal(0, db.Users.Count()); + } + + [Fact] + public void DeleteInstance_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).DeleteInstance("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/ResourceControllerTests.cs b/ManagerService.Tests/Controllers/ResourceControllerTests.cs new file mode 100644 index 0000000..82ef0fd --- /dev/null +++ b/ManagerService.Tests/Controllers/ResourceControllerTests.cs @@ -0,0 +1,144 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Services; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class ResourceControllerTests + { + private static ResourceController BuildController(MyInfoMateDbContext db) + { + var cfg = FakeMongoConfig.Create(); + var controller = new ResourceController( + NullLogger.Instance, + new ResourceDatabaseService(cfg), + new SectionDatabaseService(cfg), + new ConfigurationDatabaseService(cfg), + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test")); + return controller; + } + + // ── GET ────────────────────────────────────────────────────────────── + + [Fact] + public void Get_FiltersToInstance() + { + using var db = DbContextFactory.Create(); + db.Resources.AddRange( + new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image }, + new Resource { Id = "r2", InstanceId = "other-inst", Label = "B", Type = ResourceType.Image } + ); + db.SaveChanges(); + + var result = BuildController(db).Get("inst-test", new List()); + + var ok = Assert.IsType(result); + var resources = (ok.Value as System.Collections.IEnumerable)!.Cast().ToList(); + Assert.Single(resources); + } + + [Fact] + public void Get_WithTypeFilter_FiltersType() + { + using var db = DbContextFactory.Create(); + db.Resources.AddRange( + new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image }, + new Resource { Id = "r2", InstanceId = "inst-test", Label = "B", Type = ResourceType.Video } + ); + db.SaveChanges(); + + var result = BuildController(db).Get("inst-test", new List { ResourceType.Image }); + + var ok = Assert.IsType(result); + var resources = (ok.Value as System.Collections.IEnumerable)!.Cast().ToList(); + Assert.Single(resources); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Update_ExistingResource_UpdatesLabel() + { + using var db = DbContextFactory.Create(); + db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Old", Type = ResourceType.Image }); + db.SaveChanges(); + + var result = BuildController(db).Update(new ResourceDTO { id = "r1", label = "New Label" }); + + Assert.IsType(result); + Assert.Equal("New Label", db.Resources.First().Label); + } + + [Fact] + public void Update_UnknownResource_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Update(new ResourceDTO { id = "unknown", label = "X" }); + + Assert.IsType(result); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_NullifiesConfigurationImageId() + { + using var db = DbContextFactory.Create(); + db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Img", Type = ResourceType.Image }); + db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", ImageId = "r1", Title = new List() }); + db.SaveChanges(); + + BuildController(db).Delete("r1"); + + Assert.Null(db.Configurations.First().ImageId); + } + + [Fact] + public void Delete_NullifiesSectionImageId() + { + using var db = DbContextFactory.Create(); + db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "Img", Type = ResourceType.Image }); + db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "C", Title = new List() }); + db.Sections.Add(new Section { Id = "s1", InstanceId = "inst-test", Label = "S", ImageId = "r1", Type = SectionType.Article, ConfigurationId = "c1", Title = new List(), Description = new List() }); + db.SaveChanges(); + + BuildController(db).Delete("r1"); + + Assert.Null(db.Sections.First().ImageId); + } + + [Fact] + public void Delete_UnknownResource_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete("unknown"); + + Assert.IsType(result); + } + + [Fact] + public void Delete_ExistingResource_Returns202() + { + using var db = DbContextFactory.Create(); + db.Resources.Add(new Resource { Id = "r1", InstanceId = "inst-test", Label = "A", Type = ResourceType.Image }); + db.SaveChanges(); + + var result = BuildController(db).Delete("r1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.Resources.Count()); + } + } +} diff --git a/ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs b/ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs new file mode 100644 index 0000000..e3cbc37 --- /dev/null +++ b/ManagerService.Tests/Controllers/SectionAgendaControllerTests.cs @@ -0,0 +1,195 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.Data.SubSection; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class SectionAgendaControllerTests + { + private SectionAgendaController BuildController(MyInfoMateDbContext db) + { + var cfg = new ConfigurationBuilder().Build(); + return new SectionAgendaController(cfg, NullLogger.Instance, db); + } + + private SectionAgenda MakeSection(MyInfoMateDbContext db, string id = "sa1") + { + var section = new SectionAgenda + { + Id = id, + Label = "Agenda test", + Title = new List(), + Description = new List(), + ConfigurationId = "conf1", + InstanceId = "inst1", + Type = SectionType.Agenda, + AgendaResourceIds = new List(), + EventAgendas = new List() + }; + db.Sections.Add(section); + return section; + } + + private EventAgenda MakeEvent(string sectionId, DateTime? dateFrom) + { + return new EventAgenda + { + Label = new List(), + Description = new List(), + SectionAgendaId = sectionId, + DateFrom = dateFrom + }; + } + + // ── GET ALL ────────────────────────────────────────────────────────── + + [Fact] + public void GetAllEventAgendaFromSection_UnknownSection_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetAllEventAgendaFromSection("unknown"); + + Assert.IsType(result); + } + + [Fact] + public void GetAllEventAgendaFromSection_ReturnsAllEvents() + { + using var db = DbContextFactory.Create(); + MakeSection(db); + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-5))); + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(5))); + db.SaveChanges(); + + var result = BuildController(db).GetAllEventAgendaFromSection("sa1"); + + var ok = Assert.IsType(result); + var events = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(2, events.Count()); + } + + // ── GET UPCOMING ───────────────────────────────────────────────────── + + [Fact] + public void GetUpcomingEventAgendas_FiltersOutPastEvents() + { + using var db = DbContextFactory.Create(); + MakeSection(db); + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-1))); // passé → filtré + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today)); // aujourd'hui → inclus + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(1))); // futur → inclus + db.SaveChanges(); + + var result = BuildController(db).GetUpcomingEventAgendas("sa1"); + + var ok = Assert.IsType(result); + var events = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(2, events.Count()); + } + + [Fact] + public void GetUpcomingEventAgendas_NullDateFrom_IsIncluded() + { + using var db = DbContextFactory.Create(); + MakeSection(db); + db.EventAgendas.Add(MakeEvent("sa1", null)); // sans date → inclus + db.EventAgendas.Add(MakeEvent("sa1", DateTime.Today.AddDays(-1))); // passé → filtré + db.SaveChanges(); + + var result = BuildController(db).GetUpcomingEventAgendas("sa1"); + + var ok = Assert.IsType(result); + var events = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(1, events.Count()); + } + + [Fact] + public void GetUpcomingEventAgendas_UnknownSection_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetUpcomingEventAgendas("unknown"); + + Assert.IsType(result); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void CreateEventAgenda_ValidDto_Persists() + { + using var db = DbContextFactory.Create(); + MakeSection(db); + db.SaveChanges(); + + var dto = new EventAgendaDTO + { + label = new List(), + description = new List(), + sectionAgendaId = "sa1" + }; + + var result = BuildController(db).CreateEventAgenda("sa1", dto); + + Assert.IsType(result); + Assert.Equal(1, db.EventAgendas.Count()); + } + + [Fact] + public void CreateEventAgenda_UnknownSection_Returns500() + { + using var db = DbContextFactory.Create(); + var dto = new EventAgendaDTO + { + label = new List(), + description = new List(), + sectionAgendaId = "unknown" + }; + + // La section n'existe pas → KeyNotFoundException → ObjectResult 500 + var result = BuildController(db).CreateEventAgenda("unknown", dto); + + var status = Assert.IsType(result); + Assert.Equal(500, status.StatusCode); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void DeleteEventAgenda_ExistingEvent_Returns202() + { + using var db = DbContextFactory.Create(); + MakeSection(db); + var ev = MakeEvent("sa1", null); + db.EventAgendas.Add(ev); + db.SaveChanges(); + var id = ev.Id; + + var result = BuildController(db).DeleteEventAgenda(id); + + var status = Assert.IsType(result); + Assert.Equal(202, status.StatusCode); + Assert.Equal(0, db.EventAgendas.Count()); + } + + [Fact] + public void DeleteEventAgenda_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).DeleteEventAgenda(9999); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/SectionControllerTests.cs b/ManagerService.Tests/Controllers/SectionControllerTests.cs new file mode 100644 index 0000000..466d116 --- /dev/null +++ b/ManagerService.Tests/Controllers/SectionControllerTests.cs @@ -0,0 +1,125 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Services; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class SectionControllerTests + { + private static IConfiguration BuildConfig() => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017", + ["SupportedLanguages:0"] = "FR", + ["SupportedLanguages:1"] = "EN" + }) + .Build(); + + private static SectionController BuildController(MyInfoMateDbContext db) + { + var cfg = FakeMongoConfig.Create(); + var controller = new SectionController( + BuildConfig(), + NullLogger.Instance, + new SectionDatabaseService(cfg), + new ConfigurationDatabaseService(cfg), + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test")); + return controller; + } + + // ── GET ────────────────────────────────────────────────────────────── + + [Fact] + public void Get_FiltersToInstance() + { + using var db = DbContextFactory.Create(); + db.Sections.AddRange( + new Section { Id = "s1", InstanceId = "inst-test", Label = "A", Type = SectionType.Article, ConfigurationId = "c1", Title = new List(), Description = new List() }, + new Section { Id = "s2", InstanceId = "other-inst", Label = "B", Type = SectionType.Article, ConfigurationId = "c2", Title = new List(), Description = new List() } + ); + db.SaveChanges(); + + var result = BuildController(db).Get("inst-test"); + + var ok = Assert.IsType(result); + var sections = (ok.Value as System.Collections.IEnumerable)!.Cast().ToList(); + Assert.Single(sections); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_ValidDto_Persists() + { + using var db = DbContextFactory.Create(); + db.Configurations.Add(new Configuration { Id = "c1", InstanceId = "inst-test", Label = "Conf", Title = new List() }); + db.SaveChanges(); + + var result = BuildController(db).Create(new SectionDTO + { + instanceId = "inst-test", + configurationId = "c1", + label = "Section 1", + type = SectionType.Video + }); + + Assert.IsType(result); + Assert.Equal(1, db.Sections.Count()); + } + + [Fact] + public void Create_UnknownConfiguration_ReturnsError() + { + using var db = DbContextFactory.Create(); + + // KeyNotFoundException → caught by catch(Exception) → 500 + var result = BuildController(db).Create(new SectionDTO + { + instanceId = "inst-test", + configurationId = "nonexistent", + label = "Section 1", + type = SectionType.Video + }); + + var obj = Assert.IsType(result); + Assert.Equal(500, obj.StatusCode); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingSection_Returns202() + { + using var db = DbContextFactory.Create(); + db.Sections.Add(new Section { Id = "s1", InstanceId = "inst-test", Label = "A", Type = SectionType.Article, ConfigurationId = "c1", Title = new List(), Description = new List() }); + db.SaveChanges(); + + var result = BuildController(db).Delete("s1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.Sections.Count()); + } + + [Fact] + public void Delete_UnknownSection_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/SectionEventControllerTests.cs b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs new file mode 100644 index 0000000..6a9eb5e --- /dev/null +++ b/ManagerService.Tests/Controllers/SectionEventControllerTests.cs @@ -0,0 +1,186 @@ +using Manager.DTOs; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.Data.SubSection; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; +using static ManagerService.Data.SubSection.SectionEvent; + +namespace ManagerService.Tests.Controllers +{ + public class SectionEventControllerTests + { + private static SectionEventController BuildController(MyInfoMateDbContext db) + { + var controller = new SectionEventController( + FakeMongoConfig.Create(), + NullLogger.Instance, + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test")); + return controller; + } + + private static List EmptyTranslations() => new List(); + + // ── PROGRAMME BLOCK ────────────────────────────────────────────────── + + [Fact] + public void CreateProgrammeBlock_ValidRequest_Persists() + { + using var db = DbContextFactory.Create(); + db.Sections.Add(new SectionEvent + { + Id = "se-1", + InstanceId = "inst-test", + Label = "Event", + Type = SectionType.Event, + ConfigurationId = "c1", + Title = EmptyTranslations(), + Description = EmptyTranslations(), + Programme = new List(), + ParcoursIds = new List() + }); + db.SaveChanges(); + + var result = BuildController(db).CreateProgrammeBlock("se-1", new ProgrammeBlockDTO + { + title = EmptyTranslations(), + description = EmptyTranslations(), + startTime = DateTime.UtcNow, + endTime = DateTime.UtcNow.AddHours(1) + }); + + Assert.IsType(result); + Assert.Equal(1, db.ProgrammeBlocks.Count()); + } + + [Fact] + public void CreateProgrammeBlock_UnknownSection_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).CreateProgrammeBlock("unknown", new ProgrammeBlockDTO + { + title = EmptyTranslations(), + startTime = DateTime.UtcNow, + endTime = DateTime.UtcNow.AddHours(1) + }); + + // KeyNotFoundException attrapée par catch(Exception) → 500 + var obj = Assert.IsType(result); + Assert.Equal(500, obj.StatusCode); + } + + [Fact] + public void UpdateProgrammeBlock_ExistingBlock_UpdatesFields() + { + using var db = DbContextFactory.Create(); + db.ProgrammeBlocks.Add(new ProgrammeBlock + { + Id = "pb-1", + Title = EmptyTranslations(), + Description = EmptyTranslations(), + StartTime = DateTime.UtcNow, + EndTime = DateTime.UtcNow.AddHours(1), + MapAnnotations = new List() + }); + db.SaveChanges(); + + var newStart = DateTime.UtcNow.AddHours(2); + var result = BuildController(db).UpdateProgrammeBlock(new ProgrammeBlockDTO + { + id = "pb-1", + title = EmptyTranslations(), + description = EmptyTranslations(), + startTime = newStart, + endTime = newStart.AddHours(1) + }); + + Assert.IsType(result); + Assert.Equal(newStart, db.ProgrammeBlocks.First().StartTime); + } + + [Fact] + public void DeleteProgrammeBlock_ExistingBlock_Returns202() + { + using var db = DbContextFactory.Create(); + db.ProgrammeBlocks.Add(new ProgrammeBlock + { + Id = "pb-1", + Title = EmptyTranslations(), + Description = EmptyTranslations(), + StartTime = DateTime.UtcNow, + EndTime = DateTime.UtcNow.AddHours(1), + MapAnnotations = new List() + }); + db.SaveChanges(); + + var result = BuildController(db).DeleteProgrammeBlock("pb-1"); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.ProgrammeBlocks.Count()); + } + + // ── MAP ANNOTATION ─────────────────────────────────────────────────── + + [Fact] + public void CreateMapAnnotation_ValidRequest_Persists() + { + using var db = DbContextFactory.Create(); + db.ProgrammeBlocks.Add(new ProgrammeBlock + { + Id = "pb-1", + Title = EmptyTranslations(), + Description = EmptyTranslations(), + StartTime = DateTime.UtcNow, + EndTime = DateTime.UtcNow.AddHours(1), + MapAnnotations = new List() + }); + db.SaveChanges(); + + var result = BuildController(db).CreateMapAnnotation("pb-1", new MapAnnotationDTO + { + label = EmptyTranslations(), + type = EmptyTranslations(), + geometryType = GeometryType.Point, + geometry = new GeometryDTO { type = "Point", coordinates = new List { 4.35, 50.85 } } + }); + + Assert.IsType(result); + Assert.Equal(1, db.MapAnnotations.Count()); + } + + [Fact] + public void UpdateMapAnnotation_ExistingAnnotation_UpdatesLabel() + { + using var db = DbContextFactory.Create(); + db.MapAnnotations.Add(new MapAnnotation + { + Id = "ma-1", + Label = EmptyTranslations(), + Type = EmptyTranslations() + }); + db.SaveChanges(); + + var newLabel = new List { new TranslationDTO { language = "FR", value = "Updated" } }; + var result = BuildController(db).UpdateMapAnnotation(new MapAnnotationDTO + { + id = "ma-1", + label = newLabel, + type = EmptyTranslations(), + geometryType = GeometryType.Point, + geometry = null // UpdateMapAnnotation handles null geometry + }); + + Assert.IsType(result); + Assert.Equal("Updated", db.MapAnnotations.First().Label.First().value); + } + } +} diff --git a/ManagerService.Tests/Controllers/SectionMapControllerTests.cs b/ManagerService.Tests/Controllers/SectionMapControllerTests.cs new file mode 100644 index 0000000..d2f74f0 --- /dev/null +++ b/ManagerService.Tests/Controllers/SectionMapControllerTests.cs @@ -0,0 +1,142 @@ +using Manager.DTOs; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.Data.SubSection; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class SectionMapControllerTests + { + private static SectionMapController BuildController(MyInfoMateDbContext db) + { + var controller = new SectionMapController( + FakeMongoConfig.Create(), + NullLogger.Instance, + db); + FakeUser.SetUser(controller, FakeUser.Create("Manager.contenteditor", "inst-test")); + return controller; + } + + private static GeoPoint EmptyGeoPoint() => new GeoPoint + { + Title = new List(), + Description = new List(), + Contents = new List(), + Schedules = new List(), + Prices = new List(), + Phone = new List(), + Email = new List(), + Site = new List() + }; + + private static GeoPointDTO EmptyGeoPointDTO() => new GeoPointDTO + { + title = new List(), + description = new List(), + contents = new List(), + schedules = new List(), + prices = new List(), + phone = new List(), + email = new List(), + site = new List() + }; + + // ── GET POINTS ─────────────────────────────────────────────────────── + + [Fact] + public void GetAllGeoPoints_ReturnsPointsForSection() + { + using var db = DbContextFactory.Create(); + var section = new SectionMap + { + Id = "sm-1", + InstanceId = "inst-test", + Label = "Map", + Type = SectionType.Map, + ConfigurationId = "c1", + Title = new List(), + Description = new List(), + MapPoints = new List { EmptyGeoPoint(), EmptyGeoPoint() }, + MapCategories = new List() + }; + db.Sections.Add(section); + db.SaveChanges(); + + var result = BuildController(db).GetAllGeoPointsFromSection("sm-1"); + + var ok = Assert.IsType(result); + var points = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(2, points.Count()); + } + + [Fact] + public void GetAllGeoPoints_UnknownSection_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetAllGeoPointsFromSection("unknown"); + + Assert.IsType(result); + } + + // ── CREATE POINT ───────────────────────────────────────────────────── + + [Fact] + public void Create_ValidPoint_Persists() + { + using var db = DbContextFactory.Create(); + db.Sections.Add(new SectionMap + { + Id = "sm-1", + InstanceId = "inst-test", + Label = "Map", + Type = SectionType.Map, + ConfigurationId = "c1", + Title = new List(), + Description = new List(), + MapPoints = new List(), + MapCategories = new List() + }); + db.SaveChanges(); + + var result = BuildController(db).Create("sm-1", EmptyGeoPointDTO()); + + Assert.IsType(result); + Assert.Equal(1, db.GeoPoints.Count()); + } + + // ── DELETE POINT ───────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingGeoPoint_Returns202() + { + using var db = DbContextFactory.Create(); + db.GeoPoints.Add(EmptyGeoPoint()); + db.SaveChanges(); + + int id = (int)db.GeoPoints.First().Id!; + var result = BuildController(db).Delete(id); + + var obj = Assert.IsType(result); + Assert.Equal(202, obj.StatusCode); + Assert.Equal(0, db.GeoPoints.Count()); + } + + [Fact] + public void Delete_UnknownGeoPoint_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete(999); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/SectionQuizControllerTests.cs b/ManagerService.Tests/Controllers/SectionQuizControllerTests.cs new file mode 100644 index 0000000..dbb5332 --- /dev/null +++ b/ManagerService.Tests/Controllers/SectionQuizControllerTests.cs @@ -0,0 +1,195 @@ +using Manager.DTOs; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.Data.SubSection; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class SectionQuizControllerTests + { + private SectionQuizController BuildController(MyInfoMateDbContext db) + { + var cfg = new ConfigurationBuilder().Build(); + return new SectionQuizController(cfg, NullLogger.Instance, db); + } + + private SectionQuiz MakeQuizSection(string id = "sq1") + { + return new SectionQuiz + { + Id = id, + Label = "Quiz test", + Title = new List(), + Description = new List(), + ConfigurationId = "conf1", + InstanceId = "inst1", + Type = SectionType.Quiz, + QuizQuestions = new List(), + QuizBadLevel = new List(), + QuizMediumLevel = new List(), + QuizGoodLevel = new List(), + QuizGreatLevel = new List() + }; + } + + private QuizQuestion MakeQuestion(string sectionId, int order, string? label = null) + { + return new QuizQuestion + { + SectionQuizId = sectionId, + Order = order, + Label = new List(), + Responses = new List() + }; + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_AutoSetsOrderToCount() + { + using var db = DbContextFactory.Create(); + var section = MakeQuizSection(); + db.Sections.Add(section); + // 2 questions déjà dans la section + db.QuizQuestions.AddRange( + MakeQuestion("sq1", 0), + MakeQuestion("sq1", 1) + ); + db.SaveChanges(); + + var dto = new QuestionDTO + { + label = new List(), + responses = new List() + }; + + var result = BuildController(db).Create("sq1", dto); + + var ok = Assert.IsType(result); + // La nouvelle question doit avoir l'ordre = 2 (count avant ajout) + var question = db.QuizQuestions.OrderByDescending(q => q.Order).First(); + Assert.Equal(2, question.Order); + } + + [Fact] + public void Create_UnknownSection_Returns500() + { + using var db = DbContextFactory.Create(); + + var dto = new QuestionDTO + { + label = new List(), + responses = new List() + }; + + var result = BuildController(db).Create("unknown", dto); + + var status = Assert.IsType(result); + Assert.Equal(500, status.StatusCode); + } + + // ── UPDATE / REORDER ───────────────────────────────────────────────── + + [Fact] + public void Update_ChangeOrder_ReordersAllQuestionsFrom0() + { + using var db = DbContextFactory.Create(); + var section = MakeQuizSection(); + db.Sections.Add(section); + var q0 = MakeQuestion("sq1", 0); + var q1 = MakeQuestion("sq1", 1); + var q2 = MakeQuestion("sq1", 2); + db.QuizQuestions.AddRange(q0, q1, q2); + db.SaveChanges(); + + // Déplace q2 (order=2) à la position 0 + var dto = new QuestionDTO + { + id = q2.Id, + order = 0, + label = new List(), + responses = new List() + }; + BuildController(db).Update(dto); + + var questions = db.QuizQuestions.OrderBy(q => q.Order).ToList(); + // Après réordonnancement : q2 doit être en position 0 + Assert.Equal(q2.Id, questions[0].Id); + // Les ordres doivent être continus depuis 0 + Assert.Equal(new[] { 0, 1, 2 }, questions.Select(q => q.Order).ToArray()); + } + + [Fact] + public void Update_SameOrder_UpdatesFieldsWithoutReorder() + { + using var db = DbContextFactory.Create(); + var section = MakeQuizSection(); + db.Sections.Add(section); + var q0 = MakeQuestion("sq1", 0); + db.QuizQuestions.Add(q0); + db.SaveChanges(); + + var dto = new QuestionDTO + { + id = q0.Id, + order = 0, // même ordre + label = new List { new TranslationAndResourceDTO { language = "fr", value = "Nouvelle question" } }, + responses = new List() + }; + BuildController(db).Update(dto); + + // Le label est mis à jour (via l'objet retourné — la valeur est dans le DTO en retour) + // On vérifie juste que ça s'est exécuté sans erreur + Assert.Equal(1, db.QuizQuestions.Count()); + } + + [Fact] + public void Update_UnknownQuestion_Returns404() + { + using var db = DbContextFactory.Create(); + + var dto = new QuestionDTO { id = 9999, order = 0, label = new List(), responses = new List() }; + + var result = BuildController(db).Update(dto); + + Assert.IsType(result); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_ExistingQuestion_Returns202() + { + using var db = DbContextFactory.Create(); + var section = MakeQuizSection(); + db.Sections.Add(section); + var q = MakeQuestion("sq1", 0); + db.QuizQuestions.Add(q); + db.SaveChanges(); + var qId = q.Id; + + var result = BuildController(db).Delete(qId); + + var status = Assert.IsType(result); + Assert.Equal(202, status.StatusCode); + Assert.Equal(0, db.QuizQuestions.Count()); + } + + [Fact] + public void Delete_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete(9999); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/StatsControllerTests.cs b/ManagerService.Tests/Controllers/StatsControllerTests.cs new file mode 100644 index 0000000..8df899d --- /dev/null +++ b/ManagerService.Tests/Controllers/StatsControllerTests.cs @@ -0,0 +1,179 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class StatsControllerTests + { + private StatsController BuildController(MyInfoMateDbContext db) => new StatsController(db); + + // ── TRACK EVENT ────────────────────────────────────────────────────── + + [Fact] + public void TrackEvent_ValidDto_PersistsAndReturns204() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).TrackEvent(new VisitEventDTO + { + instanceId = "i1", + sessionId = "s1", + eventType = "SectionView", + appType = "Mobile" + }); + + Assert.IsType(result); + Assert.Equal(1, db.VisitEvents.Count()); + } + + [Fact] + public void TrackEvent_MissingInstanceId_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).TrackEvent(new VisitEventDTO + { + instanceId = "", + eventType = "SectionView", + appType = "Mobile" + }); + + Assert.IsType(result); + } + + [Fact] + public void TrackEvent_UnknownEventType_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).TrackEvent(new VisitEventDTO + { + instanceId = "i1", + eventType = "InvalidType", + appType = "Mobile" + }); + + Assert.IsType(result); + } + + // ── GET SUMMARY ────────────────────────────────────────────────────── + + [Fact] + public void GetSummary_MissingInstanceId_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetSummary("", null, null, null); + + Assert.IsType(result); + } + + [Fact] + public void GetSummary_FiltersToInstance() + { + using var db = DbContextFactory.Create(); + db.VisitEvents.AddRange( + new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }, + new VisitEvent { Id = "e2", InstanceId = "other", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow } + ); + db.SaveChanges(); + + var result = BuildController(db).GetSummary("i1", null, null, null); + + var ok = Assert.IsType(result); + var summary = Assert.IsType(ok.Value); + Assert.Equal(1, summary.TotalSessions); + } + + [Fact] + public void GetSummary_CountsDistinctSessions() + { + using var db = DbContextFactory.Create(); + db.VisitEvents.AddRange( + new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }, + new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "session-A", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }, + new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "session-B", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow } + ); + db.SaveChanges(); + + var result = BuildController(db).GetSummary("i1", null, null, null); + + var ok = Assert.IsType(result); + var summary = Assert.IsType(ok.Value); + Assert.Equal(2, summary.TotalSessions); + } + + [Fact] + public void GetSummary_AppliesDateRange() + { + using var db = DbContextFactory.Create(); + var inRange = DateTime.UtcNow.AddDays(-5); + var outOfRange = DateTime.UtcNow.AddDays(-40); + + db.VisitEvents.AddRange( + new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, Timestamp = inRange }, + new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, Timestamp = outOfRange } + ); + db.SaveChanges(); + + var from = DateTime.UtcNow.AddDays(-10); + var to = DateTime.UtcNow; + + var result = BuildController(db).GetSummary("i1", from, to, null); + + var ok = Assert.IsType(result); + var summary = Assert.IsType(ok.Value); + Assert.Equal(1, summary.TotalSessions); + } + + [Fact] + public void GetSummary_DefaultsTo30Days() + { + using var db = DbContextFactory.Create(); + // Event dans les 30 jours + db.VisitEvents.Add(new VisitEvent + { + Id = "e1", InstanceId = "i1", SessionId = "s1", + EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-5) + }); + // Event au-delà des 30 jours + db.VisitEvents.Add(new VisitEvent + { + Id = "e2", InstanceId = "i1", SessionId = "s2", + EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow.AddDays(-35) + }); + db.SaveChanges(); + + // Sans dates → filtre 30 derniers jours + var result = BuildController(db).GetSummary("i1", null, null, null); + + var ok = Assert.IsType(result); + var summary = Assert.IsType(ok.Value); + Assert.Equal(1, summary.TotalSessions); + } + + [Fact] + public void GetSummary_TopSections_AggregatesByViewCount() + { + using var db = DbContextFactory.Create(); + db.VisitEvents.AddRange( + new VisitEvent { Id = "e1", InstanceId = "i1", SessionId = "s1", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow }, + new VisitEvent { Id = "e2", InstanceId = "i1", SessionId = "s2", EventType = VisitEventType.SectionView, SectionId = "sect-a", Timestamp = DateTime.UtcNow }, + new VisitEvent { Id = "e3", InstanceId = "i1", SessionId = "s3", EventType = VisitEventType.SectionView, SectionId = "sect-b", Timestamp = DateTime.UtcNow } + ); + db.SaveChanges(); + + var result = BuildController(db).GetSummary("i1", null, null, null); + + var ok = Assert.IsType(result); + var summary = Assert.IsType(ok.Value); + Assert.Equal(2, summary.TopSections.Count); + Assert.Equal("sect-a", summary.TopSections.First().SectionId); + Assert.Equal(2, summary.TopSections.First().Views); + } + } +} diff --git a/ManagerService.Tests/Controllers/SubscriptionPlanControllerTests.cs b/ManagerService.Tests/Controllers/SubscriptionPlanControllerTests.cs new file mode 100644 index 0000000..5fdd368 --- /dev/null +++ b/ManagerService.Tests/Controllers/SubscriptionPlanControllerTests.cs @@ -0,0 +1,169 @@ +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class SubscriptionPlanControllerTests + { + private SubscriptionPlanController BuildController(MyInfoMateDbContext db) => + new SubscriptionPlanController(NullLogger.Instance, db); + + // ── GET ────────────────────────────────────────────────────────────── + + [Fact] + public void Get_ReturnsAllPlans() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.AddRange( + new SubscriptionPlan { Id = "p1", Name = "Starter" }, + new SubscriptionPlan { Id = "p2", Name = "Standard" } + ); + db.SaveChanges(); + + var result = BuildController(db).Get(); + + var ok = Assert.IsType(result); + var plans = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(2, plans.Count()); + } + + [Fact] + public void GetById_ExistingId_ReturnsDto() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan + { + Id = "p1", Name = "Starter", StorageQuotaBytes = 1024, AiRequestsPerMonth = 50 + }); + db.SaveChanges(); + + var result = BuildController(db).GetById("p1"); + + var ok = Assert.IsType(result); + var dto = Assert.IsType(ok.Value); + Assert.Equal("Starter", dto.name); + Assert.Equal(1024, dto.storageQuotaBytes); + Assert.Equal(50, dto.aiRequestsPerMonth); + } + + [Fact] + public void GetById_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).GetById("unknown"); + + Assert.IsType(result); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Create_ValidDto_PersistsAndReturnsDto() + { + using var db = DbContextFactory.Create(); + var dto = new SubscriptionPlanDTO { name = "Premium", storageQuotaBytes = 5000, aiRequestsPerMonth = 100 }; + + var result = BuildController(db).Create(dto); + + var ok = Assert.IsType(result); + var returned = Assert.IsType(ok.Value); + Assert.Equal("Premium", returned.name); + Assert.NotNull(returned.id); + Assert.Equal(1, db.SubscriptionPlans.Count()); + } + + [Fact] + public void Create_NullDto_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Create(null!); + + Assert.IsType(result); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void Update_ExistingPlan_UpdatesName() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Old" }); + db.SaveChanges(); + + var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "p1", name = "New", storageQuotaBytes = 0, aiRequestsPerMonth = 0 }); + + Assert.IsType(result); + Assert.Equal("New", db.SubscriptionPlans.First().Name); + } + + [Fact] + public void Update_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Update(new SubscriptionPlanDTO { id = "unknown", name = "X" }); + + Assert.IsType(result); + } + + [Fact] + public void Update_NullDto_Returns400() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Update(null!); + + Assert.IsType(result); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void Delete_UnusedPlan_Returns202() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" }); + db.SaveChanges(); + + var result = BuildController(db).Delete("p1"); + + var statusResult = Assert.IsType(result); + Assert.Equal(202, statusResult.StatusCode); + Assert.Equal(0, db.SubscriptionPlans.Count()); + } + + [Fact] + public void Delete_PlanUsedByInstance_Returns409() + { + using var db = DbContextFactory.Create(); + db.SubscriptionPlans.Add(new SubscriptionPlan { Id = "p1", Name = "Starter" }); + db.Instances.Add(new Instance + { + Id = "inst1", Name = "Musée", SubscriptionPlanId = "p1", + DateCreation = DateTime.UtcNow + }); + db.SaveChanges(); + + var result = BuildController(db).Delete("p1"); + + Assert.IsType(result); + } + + [Fact] + public void Delete_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).Delete("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Controllers/UserControllerTests.cs b/ManagerService.Tests/Controllers/UserControllerTests.cs new file mode 100644 index 0000000..a0f7517 --- /dev/null +++ b/ManagerService.Tests/Controllers/UserControllerTests.cs @@ -0,0 +1,185 @@ +using Manager.Services; +using ManagerService.Controllers; +using ManagerService.Data; +using ManagerService.DTOs; +using ManagerService.Helpers; +using ManagerService.Tests.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace ManagerService.Tests.Controllers +{ + public class UserControllerTests + { + private UserController BuildController(MyInfoMateDbContext db, string callerRole = "Manager.superadmin", string callerInstanceId = "inst-test") + { + var cfg = FakeMongoConfig.Create(); + var userService = new UserDatabaseService(cfg); + var profileLogic = new ProfileLogic(NullLogger.Instance); + + var controller = new UserController( + NullLogger.Instance, + userService, + profileLogic, + db); + + FakeUser.SetUser(controller, FakeUser.Create(callerRole, callerInstanceId)); + return controller; + } + + private static UserDetailDTO MakeNewUser(string email, string instanceId = "inst-test", UserRole role = UserRole.ContentEditor) => + new UserDetailDTO + { + email = email, + firstName = "Test", + lastName = "User", + instanceId = instanceId, + password = "password123", + role = role + }; + + // ── GET ────────────────────────────────────────────────────────────── + + [Fact] + public void Get_SuperAdmin_SeesAllInstances() + { + using var db = DbContextFactory.Create(); + db.Users.AddRange( + new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst1" }, + new User { Id = "u2", Email = "b@b.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "inst2" } + ); + db.SaveChanges(); + + var result = BuildController(db, "Manager.superadmin").Get(); + + var ok = Assert.IsType(result); + var users = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(2, users.Count()); + } + + [Fact] + public void Get_InstanceAdmin_SeesOnlyOwnInstance() + { + using var db = DbContextFactory.Create(); + db.Users.AddRange( + new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" }, + new User { Id = "u2", Email = "b@b.be", Password = "x", LastName = "B", Token = "t2", InstanceId = "other-inst" } + ); + db.SaveChanges(); + + var result = BuildController(db, "Manager.instanceadmin", "inst-test").Get(); + + var ok = Assert.IsType(result); + var users = Assert.IsAssignableFrom>(ok.Value); + Assert.Equal(1, users.Count()); + Assert.All(users, u => Assert.Equal("inst-test", u.instanceId)); + } + + // ── CREATE ─────────────────────────────────────────────────────────── + + [Fact] + public void CreateUser_DuplicateEmail_Returns409() + { + using var db = DbContextFactory.Create(); + db.Users.Add(new User { Id = "u1", Email = "dup@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" }); + db.SaveChanges(); + + var result = BuildController(db).CreateUser(MakeNewUser("dup@a.be")); + + Assert.IsType(result); + } + + [Fact] + public void CreateUser_InstanceAdminCreatingInstanceAdmin_Returns403() + { + // InstanceAdmin (role=1) ne peut pas créer SuperAdmin (role=0) + using var db = DbContextFactory.Create(); + + var result = BuildController(db, "Manager.instanceadmin") + .CreateUser(MakeNewUser("new@a.be", role: UserRole.SuperAdmin)); + + var statusResult = Assert.IsType(result); + Assert.Equal(403, statusResult.StatusCode); + } + + [Fact] + public void CreateUser_ValidDto_HashesPassword() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).CreateUser(MakeNewUser("new@a.be")); + + Assert.IsType(result); + var user = db.Users.First(); + Assert.NotEqual("password123", user.Password); // doit être haché + } + + [Fact] + public void CreateUser_NullPassword_Returns400() + { + using var db = DbContextFactory.Create(); + + var dto = new UserDetailDTO { email = "x@x.be", lastName = "X", instanceId = "inst-test", password = null }; + var result = BuildController(db).CreateUser(dto); + + Assert.IsType(result); + } + + // ── UPDATE ─────────────────────────────────────────────────────────── + + [Fact] + public void UpdateUser_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).UpdateUser(new UserDetailDTO { id = "unknown", firstName = "X", lastName = "Y" }); + + Assert.IsType(result); + } + + [Fact] + public void UpdateUser_ExistingUser_UpdatesName() + { + using var db = DbContextFactory.Create(); + db.Users.Add(new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "Old", Token = "t1", InstanceId = "inst-test" }); + db.SaveChanges(); + + var result = BuildController(db).UpdateUser(new UserDetailDTO { id = "u1", firstName = "New", lastName = "Name" }); + + Assert.IsType(result); + var user = db.Users.First(); + Assert.Equal("New", user.FirstName); + Assert.Equal("Name", user.LastName); + } + + // ── DELETE ─────────────────────────────────────────────────────────── + + [Fact] + public void DeleteUser_ExistingUser_Returns202() + { + using var db = DbContextFactory.Create(); + db.Users.Add(new User { Id = "u1", Email = "a@a.be", Password = "x", LastName = "A", Token = "t1", InstanceId = "inst-test" }); + db.SaveChanges(); + + var result = BuildController(db).DeleteUser("u1"); + + var statusResult = Assert.IsType(result); + Assert.Equal(202, statusResult.StatusCode); + Assert.Equal(0, db.Users.Count()); + } + + [Fact] + public void DeleteUser_UnknownId_Returns404() + { + using var db = DbContextFactory.Create(); + + var result = BuildController(db).DeleteUser("unknown"); + + Assert.IsType(result); + } + } +} diff --git a/ManagerService.Tests/Infrastructure/DbContextFactory.cs b/ManagerService.Tests/Infrastructure/DbContextFactory.cs new file mode 100644 index 0000000..d86f279 --- /dev/null +++ b/ManagerService.Tests/Infrastructure/DbContextFactory.cs @@ -0,0 +1,17 @@ +using ManagerService.Data; +using Microsoft.EntityFrameworkCore; + +namespace ManagerService.Tests.Infrastructure +{ + public static class DbContextFactory + { + public static MyInfoMateDbContext Create() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(System.Guid.NewGuid().ToString()) + .Options; + + return new MyInfoMateDbContext(options); + } + } +} diff --git a/ManagerService.Tests/Infrastructure/FakeMongoConfig.cs b/ManagerService.Tests/Infrastructure/FakeMongoConfig.cs new file mode 100644 index 0000000..5625b1f --- /dev/null +++ b/ManagerService.Tests/Infrastructure/FakeMongoConfig.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; + +namespace ManagerService.Tests.Infrastructure +{ + /// + /// IConfiguration avec une fausse connexion MongoDB pour instancier les + /// DatabaseService dont le code actif des contrôleurs ne se sert plus + /// (remplacé par EF Core), mais dont le constructeur en a besoin. + /// MongoClient est lazy : aucune connexion réelle ne sera tentée. + /// + public static class FakeMongoConfig + { + public static IConfiguration Create() => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["ConnectionStrings:TabletDb"] = "mongodb://localhost:27017" + }) + .Build(); + } +} diff --git a/ManagerService.Tests/Infrastructure/FakeUser.cs b/ManagerService.Tests/Infrastructure/FakeUser.cs new file mode 100644 index 0000000..ce6e0c3 --- /dev/null +++ b/ManagerService.Tests/Infrastructure/FakeUser.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Security.Claims; + +namespace ManagerService.Tests.Infrastructure +{ + // Constantes reproduisant ManagerService.Service.Security (internal) + public static class Permissions + { + public const string SuperAdmin = "Manager.superadmin"; + public const string InstanceAdmin = "Manager.instanceadmin"; + public const string ContentEditor = "Manager.contenteditor"; + public const string Viewer = "Manager.viewer"; + } + + public static class ClaimTypeKeys + { + public const string Permission = "Permission"; + public const string InstanceId = "InstanceId"; + } + + public static class FakeUser + { + public static ClaimsPrincipal Create(string role, string instanceId = "inst-test") + { + var claims = new List + { + new Claim(ClaimTypeKeys.Permission, role), + new Claim(ClaimTypeKeys.InstanceId, instanceId) + }; + + // Rôles cumulatifs : SuperAdmin hérite de tout + if (role == Permissions.SuperAdmin) + { + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.InstanceAdmin)); + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.ContentEditor)); + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer)); + } + else if (role == Permissions.InstanceAdmin) + { + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.ContentEditor)); + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer)); + } + else if (role == Permissions.ContentEditor) + { + claims.Add(new Claim(ClaimTypeKeys.Permission, Permissions.Viewer)); + } + + return new ClaimsPrincipal(new ClaimsIdentity(claims, "test")); + } + + public static void SetUser(ControllerBase controller, ClaimsPrincipal user) + { + controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = user } + }; + } + } +} diff --git a/ManagerService.Tests/ManagerService.Tests.csproj b/ManagerService.Tests/ManagerService.Tests.csproj new file mode 100644 index 0000000..2a8135d --- /dev/null +++ b/ManagerService.Tests/ManagerService.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + false + true + enable + enable + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/ManagerService.Tests/nuget.config b/ManagerService.Tests/nuget.config new file mode 100644 index 0000000..765346e --- /dev/null +++ b/ManagerService.Tests/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ManagerService.sln b/ManagerService.sln index d65c6f7..2e42817 100644 --- a/ManagerService.sln +++ b/ManagerService.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.12.35527.113 d17.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagerService", "ManagerService\ManagerService.csproj", "{042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagerService.Tests", "ManagerService.Tests\ManagerService.Tests.csproj", "{C95DA377-C16F-457E-AC51-C8CCC7BC562D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Debug|Any CPU.Build.0 = Debug|Any CPU {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.ActiveCfg = Release|Any CPU {042E0BC4-8DCF-4EEC-8420-C71AA85D4D99}.Release|Any CPU.Build.0 = Release|Any CPU + {C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C95DA377-C16F-457E-AC51-C8CCC7BC562D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ManagerService/AssemblyInfo.cs b/ManagerService/AssemblyInfo.cs new file mode 100644 index 0000000..2f2b20a --- /dev/null +++ b/ManagerService/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("ManagerService.Tests")] diff --git a/ManagerService/Controllers/AiController.cs b/ManagerService/Controllers/AiController.cs index 523f4d9..7da302c 100644 --- a/ManagerService/Controllers/AiController.cs +++ b/ManagerService/Controllers/AiController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NSwag.Annotations; +using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Threading.Tasks; @@ -16,12 +17,12 @@ namespace ManagerService.Controllers [OpenApiTag("AI", Description = "Assistant IA")] public class AiController : ControllerBase { - private readonly AssistantService _assistantService; + private readonly IAssistantService _assistantService; private readonly MyInfoMateDbContext _context; private readonly ILogger _logger; public AiController( - AssistantService assistantService, + IAssistantService assistantService, MyInfoMateDbContext context, ILogger logger) { @@ -30,6 +31,31 @@ namespace ManagerService.Controllers _logger = logger; } + /// + /// Traduit un texte HTML vers plusieurs langues via IA + /// + [HttpPost("translate")] + [ProducesResponseType(typeof(AiTranslateResponse), 200)] + [ProducesResponseType(403)] + [ProducesResponseType(typeof(string), 500)] + public async Task Translate([FromBody] AiTranslateRequest request, [FromQuery] string instanceId) + { + try + { + var instance = _context.Instances.FirstOrDefault(i => i.Id == instanceId); + if (instance == null || !instance.IsAssistant) + return Forbid(); + + var result = await _assistantService.TranslateAsync(request); + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erreur lors de la traduction IA"); + return new ObjectResult("Une erreur est survenue") { StatusCode = 500 }; + } + } + /// /// Envoie un message à l'assistant IA, scopé à l'instance et optionnellement à une configuration /// @@ -42,7 +68,8 @@ namespace ManagerService.Controllers try { // Vérifie que l'instance a activé la fonctionnalité assistant - var instance = _context.Instances.FirstOrDefault(i => i.Id == request.InstanceId); + var instance = _context.Instances + .FirstOrDefault(i => i.Id == request.InstanceId); if (instance == null || !instance.IsAssistant) return Forbid(); @@ -59,6 +86,11 @@ namespace ManagerService.Controllers instance.AiRequestsThisMonth = 0; instance.AiUsageMonthKey = monthKey; } + + var quota = instance.AiRequestsPerMonth; + if (quota > 0 && instance.AiRequestsThisMonth >= quota) + return StatusCode(429, "Quota IA mensuel dépassé"); + instance.AiRequestsThisMonth++; _context.SaveChanges(); diff --git a/ManagerService/Controllers/AuditController.cs b/ManagerService/Controllers/AuditController.cs new file mode 100644 index 0000000..2bea429 --- /dev/null +++ b/ManagerService/Controllers/AuditController.cs @@ -0,0 +1,64 @@ +using ManagerService.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using NSwag.Annotations; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace ManagerService.Controllers +{ + [Authorize(Policy = ManagerService.Service.Security.Policies.SuperAdmin)] + [ApiController, Route("api/[controller]")] + [OpenApiTag("Audit", Description = "Audit trail — SuperAdmin only")] + public class AuditController : ControllerBase + { + private readonly MyInfoMateDbContext _db; + private readonly ILogger _logger; + + public AuditController(MyInfoMateDbContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + + [HttpGet] + public async Task GetAuditLogs( + [FromQuery] string? instanceId, + [FromQuery] string? entityType, + [FromQuery] string? userId, + [FromQuery] DateTime? from, + [FromQuery] DateTime? to, + [FromQuery] int page = 1, + [FromQuery] int limit = 50) + { + var query = _db.AuditLogs.AsQueryable(); + + if (!string.IsNullOrEmpty(instanceId)) + query = query.Where(a => a.InstanceId == instanceId); + + if (!string.IsNullOrEmpty(entityType)) + query = query.Where(a => a.EntityType == entityType); + + if (!string.IsNullOrEmpty(userId)) + query = query.Where(a => a.UserId == userId); + + if (from.HasValue) + query = query.Where(a => a.Timestamp >= from.Value); + + if (to.HasValue) + query = query.Where(a => a.Timestamp <= to.Value); + + var total = await query.CountAsync(); + var items = await query + .OrderByDescending(a => a.Timestamp) + .Skip((page - 1) * limit) + .Take(limit) + .ToListAsync(); + + return Ok(new { total, page, limit, items }); + } + } +} diff --git a/ManagerService/Controllers/InstanceController.cs b/ManagerService/Controllers/InstanceController.cs index 83fdbf1..510ddad 100644 --- a/ManagerService/Controllers/InstanceController.cs +++ b/ManagerService/Controllers/InstanceController.cs @@ -116,6 +116,20 @@ namespace ManagerService.Controllers instance.DateCreation = DateTime.Now.ToUniversalTime(); instance.Id = idService.GenerateHexId(); + // Copier les valeurs du plan comme valeurs par défaut + if (instance.SubscriptionPlanId != null) + { + var plan = _myInfoMateDbContext.SubscriptionPlans.FirstOrDefault(p => p.Id == instance.SubscriptionPlanId); + if (plan != null) + { + instance.StorageQuotaBytes = plan.StorageQuotaBytes; + instance.AiRequestsPerMonth = plan.AiRequestsPerMonth; + instance.HasStats = plan.HasStats; + instance.StatsHistoryDays = plan.StatsHistoryDays; + instance.HasAdvancedStats = plan.HasAdvancedStats; + } + } + /*List instances = _instanceService.GetAll(); Instance instance = _myInfoMateDbContext.Instances.FirstOrDefault(i => i.Id == id);*/ @@ -168,9 +182,16 @@ namespace ManagerService.Controllers if (instance == null) throw new KeyNotFoundException("instance does not exist"); - instance.DateCreation = updatedInstance.dateCreation != null ? updatedInstance.dateCreation.Value : instance.DateCreation; - instance.Name= updatedInstance.name != null ? updatedInstance.name : instance.Name; - instance.PinCode = updatedInstance.pinCode != null ? updatedInstance.pinCode : instance.PinCode; + instance.DateCreation = updatedInstance.dateCreation ?? instance.DateCreation; + instance.Name = updatedInstance.name ?? instance.Name; + instance.PinCode = updatedInstance.pinCode ?? instance.PinCode; + instance.IsPushNotification = updatedInstance.isPushNotification ?? instance.IsPushNotification; + instance.IsStatistic = updatedInstance.isStatistic ?? instance.IsStatistic; + instance.IsMobile = updatedInstance.isMobile ?? instance.IsMobile; + instance.IsTablet = updatedInstance.isTablet ?? instance.IsTablet; + instance.IsWeb = updatedInstance.isWeb ?? instance.IsWeb; + instance.IsVR = updatedInstance.isVR ?? instance.IsVR; + instance.IsAssistant = updatedInstance.isAssistant ?? instance.IsAssistant; if (updatedInstance.subscriptionPlanId == "") instance.SubscriptionPlanId = null; else if (updatedInstance.subscriptionPlanId != null) @@ -272,7 +293,6 @@ namespace ManagerService.Controllers try { var instance = _myInfoMateDbContext.Instances - .Include(i => i.SubscriptionPlan) .FirstOrDefault(i => i.Id == id); if (instance == null) @@ -288,9 +308,9 @@ namespace ManagerService.Controllers return new OkObjectResult(new InstanceQuotaDTO { storageUsedBytes = storageUsed, - storageQuotaBytes = instance.SubscriptionPlan?.StorageQuotaBytes ?? 0, + storageQuotaBytes = instance.StorageQuotaBytes, aiRequestsUsed = aiUsed, - aiRequestsPerMonth = instance.SubscriptionPlan?.AiRequestsPerMonth ?? 0 + aiRequestsPerMonth = instance.AiRequestsPerMonth }); } catch (Exception ex) diff --git a/ManagerService/Controllers/MigrationController.cs b/ManagerService/Controllers/MigrationController.cs index 2c1ebdf..e1b4170 100644 --- a/ManagerService/Controllers/MigrationController.cs +++ b/ManagerService/Controllers/MigrationController.cs @@ -11,6 +11,7 @@ using NSwag.Annotations; using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -28,6 +29,7 @@ namespace ManagerService.Controllers private readonly ResourceDatabaseService _resourceSvc; private readonly UserDatabaseService _userSvc; private readonly DeviceDatabaseService _deviceSvc; + private readonly IHttpClientFactory _httpClientFactory; public MigrationController( MyInfoMateDbContext db, @@ -36,7 +38,8 @@ namespace ManagerService.Controllers SectionDatabaseService sectionSvc, ResourceDatabaseService resourceSvc, UserDatabaseService userSvc, - DeviceDatabaseService deviceSvc) + DeviceDatabaseService deviceSvc, + IHttpClientFactory httpClientFactory) { _db = db; _instanceSvc = instanceSvc; @@ -45,6 +48,7 @@ namespace ManagerService.Controllers _resourceSvc = resourceSvc; _userSvc = userSvc; _deviceSvc = deviceSvc; + _httpClientFactory = httpClientFactory; } /// @@ -131,6 +135,30 @@ namespace ManagerService.Controllers : _instanceSvc.GetAll().Select(i => i.Id).ToList(); var source = instanceIds.SelectMany(id => _resourceSvc.GetAll(id)).ToList(); + // Fetch sizes en parallèle par batches de 30 + var httpClient = _httpClientFactory.CreateClient(); + httpClient.Timeout = TimeSpan.FromSeconds(10); + + var sizemap = new System.Collections.Concurrent.ConcurrentDictionary(); + var urlSource = source.Where(r => !string.IsNullOrEmpty(r.Url)).ToList(); + + const int batchSize = 30; + for (int i = 0; i < urlSource.Count; i += batchSize) + { + var batch = urlSource.Skip(i).Take(batchSize); + await Task.WhenAll(batch.Select(async r => + { + try + { + var req = new HttpRequestMessage(HttpMethod.Head, r.Url); + var resp = await httpClient.SendAsync(req); + if (resp.Content.Headers.ContentLength.HasValue) + sizemap[r.Id] = resp.Content.Headers.ContentLength.Value; + } + catch { /* URL inaccessible ou expirée → SizeBytes restera 0 */ } + })); + } + foreach (var old in source) { try @@ -148,8 +176,8 @@ namespace ManagerService.Controllers Label = old.Label, DateCreation = old.DateCreation, InstanceId = old.InstanceId, - Url = old.Url, // NOTE: MongoDB field is "URL", mapped to Url in OldResource - SizeBytes = 0, + Url = old.Url, + SizeBytes = sizemap.TryGetValue(old.Id, out var size) ? size : 0, }; if (!dryRun) diff --git a/ManagerService/Controllers/ResourceController.cs b/ManagerService/Controllers/ResourceController.cs index 4103818..b6c0260 100644 --- a/ManagerService/Controllers/ResourceController.cs +++ b/ManagerService/Controllers/ResourceController.cs @@ -234,7 +234,21 @@ namespace ManagerService.Controllers var resourceType = (ResourceType)Enum.Parse(typeof(ResourceType), type); List resources = new List(); - foreach (var file in Request.Form.Files) + var instance = _myInfoMateDbContext.Instances + .Include(i => i.SubscriptionPlan) + .FirstOrDefault(i => i.Id == instanceId); + var storageQuota = instance?.SubscriptionPlan?.StorageQuotaBytes ?? 0; + if (storageQuota > 0) + { + var storageUsed = _myInfoMateDbContext.Resources + .Where(r => r.InstanceId == instanceId) + .Sum(r => (long?)r.SizeBytes) ?? 0; + var incomingBytes = Request.Form.Files.Sum(f => f.Length); + if (storageUsed + incomingBytes > storageQuota) + return new ObjectResult("Quota de stockage dépassé") { StatusCode = 413 }; + } + + foreach (var file in Request.Form.Files) { if (file.Length > 0) { @@ -372,8 +386,9 @@ namespace ManagerService.Controllers resource.InstanceId = updatedResource.instanceId != null ? updatedResource.instanceId : resource.InstanceId; resource.Label = updatedResource.label != null ? updatedResource.label : resource.Label; resource.Type = updatedResource.type != null ? updatedResource.type : resource.Type; - resource.Url = updatedResource.url != null ? updatedResource.url: resource.Url; - //resource.Data = updatedResource.data; // NOT ALLOWED + resource.Url = updatedResource.url != null ? updatedResource.url : resource.Url; + if (updatedResource.sizeBytes > 0) + resource.SizeBytes = updatedResource.sizeBytes; _myInfoMateDbContext.SaveChanges(); diff --git a/ManagerService/Controllers/StatsController.cs b/ManagerService/Controllers/StatsController.cs index 9f2649e..80c938a 100644 --- a/ManagerService/Controllers/StatsController.cs +++ b/ManagerService/Controllers/StatsController.cs @@ -8,6 +8,7 @@ using ManagerService.Helpers; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using NSwag.Annotations; namespace ManagerService.Controllers @@ -80,7 +81,21 @@ namespace ManagerService.Controllers if (string.IsNullOrEmpty(instanceId)) return BadRequest("instanceId is required"); - var fromDate = (from ?? DateTime.UtcNow.AddDays(-30)).ToUniversalTime(); + var instance = _db.Instances + .FirstOrDefault(i => i.Id == instanceId); + + if (instance != null && !instance.HasStats) + return Ok(new StatsSummaryDTO()); + + var historyDays = instance?.StatsHistoryDays ?? 30; + var hasAdvancedStats = instance?.HasAdvancedStats ?? false; + + var maxFrom = historyDays > 0 + ? DateTime.UtcNow.AddDays(-historyDays) + : DateTime.MinValue; + + var requestedFrom = (from ?? DateTime.UtcNow.AddDays(-30)).ToUniversalTime(); + var fromDate = historyDays > 0 && requestedFrom < maxFrom ? maxFrom : requestedFrom; var toDate = (to ?? DateTime.UtcNow).ToUniversalTime(); var eventsQuery = _db.VisitEvents @@ -106,6 +121,18 @@ namespace ManagerService.Controllers summary.AvgVisitDurationSeconds = (int)sessionsWithDuration.Average(); } + // Section title lookup (FR preferred, fallback to first available) + var allSectionIds = events.Where(e => e.SectionId != null).Select(e => e.SectionId!).Distinct().ToList(); + var sectionTitles = _db.Sections + .Where(s => allSectionIds.Contains(s.Id)) + .Select(s => new { s.Id, s.Title }) + .ToList() + .ToDictionary( + s => s.Id, + s => s.Title?.FirstOrDefault(t => t.language == "fr")?.value + ?? s.Title?.FirstOrDefault()?.value + ); + // Top sections var sectionViews = events .Where(e => e.EventType == VisitEventType.SectionView && e.SectionId != null) @@ -119,7 +146,7 @@ namespace ManagerService.Controllers return new SectionStatDTO { SectionId = g.Key, - SectionTitle = g.Key, // apps can enrich with section titles client-side + SectionTitle = sectionTitles.GetValueOrDefault(g.Key), Views = g.Count(), AvgDurationSeconds = leaveDurations.Any() ? (int)leaveDurations.Average() : 0 }; @@ -143,15 +170,19 @@ namespace ManagerService.Controllers .OrderBy(d => d.Date) .ToList(); - // Language distribution + // Language distribution (one entry per distinct session) summary.LanguageDistribution = events .Where(e => e.Language != null) - .GroupBy(e => e.Language!) + .GroupBy(e => e.SessionId) + .Select(g => g.First().Language!) + .GroupBy(lang => lang) .ToDictionary(g => g.Key, g => g.Count()); - // AppType distribution + // AppType distribution (one entry per distinct session) summary.AppTypeDistribution = events - .GroupBy(e => e.AppType.ToString()) + .GroupBy(e => e.SessionId) + .Select(g => g.First().AppType.ToString()) + .GroupBy(t => t) .ToDictionary(g => g.Key, g => g.Count()); // Top POIs @@ -227,7 +258,7 @@ namespace ManagerService.Controllers return new QuizStatDTO { SectionId = g.Key, - SectionTitle = g.Key, + SectionTitle = sectionTitles.GetValueOrDefault(g.Key), Completions = g.Count(), AvgScore = scores.Any() ? scores.Average(s => s.score) : 0, TotalQuestions = scores.Any() ? (int)scores.Average(s => s.total) : 0 @@ -316,6 +347,18 @@ namespace ManagerService.Controllers } summary.QrScans = new QrScanStatDTO { TotalScans = qrEvents.Count, ValidScans = validQr, InvalidScans = invalidQr }; + if (!hasAdvancedStats) + { + summary.LanguageDistribution = new Dictionary(); + summary.TopPois = new List(); + summary.TopAgendaEvents = new List(); + summary.QuizStats = new List(); + summary.GameStats = new List(); + summary.TopArticles = new List(); + summary.TopMenuItems = new List(); + summary.QrScans = new QrScanStatDTO { TotalScans = 0, ValidScans = 0, InvalidScans = 0 }; + } + return Ok(summary); } catch (Exception ex) diff --git a/ManagerService/Controllers/SubscriptionPlanController.cs b/ManagerService/Controllers/SubscriptionPlanController.cs index 156317b..8521cbb 100644 --- a/ManagerService/Controllers/SubscriptionPlanController.cs +++ b/ManagerService/Controllers/SubscriptionPlanController.cs @@ -27,6 +27,7 @@ namespace ManagerService.Controllers _myInfoMateDbContext = myInfoMateDbContext; } + [Authorize(Policy = ManagerService.Service.Security.Policies.Viewer)] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(string), 500)] [HttpGet] diff --git a/ManagerService/DTOs/AiChatDTO.cs b/ManagerService/DTOs/AiChatDTO.cs index b234756..63206d8 100644 --- a/ManagerService/DTOs/AiChatDTO.cs +++ b/ManagerService/DTOs/AiChatDTO.cs @@ -40,4 +40,16 @@ namespace ManagerService.DTOs public List? Cards { get; set; } public NavigationActionDTO? Navigation { get; set; } } + + public class AiTranslateRequest + { + public string Text { get; set; } + public string SourceLang { get; set; } + public List TargetLangs { get; set; } = new(); + } + + public class AiTranslateResponse + { + public Dictionary Translations { get; set; } = new(); + } } diff --git a/ManagerService/DTOs/InstanceDTO.cs b/ManagerService/DTOs/InstanceDTO.cs index eba457e..4e40bf3 100644 --- a/ManagerService/DTOs/InstanceDTO.cs +++ b/ManagerService/DTOs/InstanceDTO.cs @@ -9,19 +9,24 @@ namespace ManagerService.DTOs public string name { get; set; } public DateTime? dateCreation { get; set; } public string pinCode { get; set; } - public bool isPushNotification { get; set; } - public bool isStatistic { get; set; } - public bool isMobile { get; set; } - public bool isTablet { get; set; } - public bool isWeb { get; set; } - public bool isVR { get; set; } + public bool? isPushNotification { get; set; } + public bool? isStatistic { get; set; } + public bool? isMobile { get; set; } + public bool? isTablet { get; set; } + public bool? isWeb { get; set; } + public bool? isVR { get; set; } - public bool isAssistant { get; set; } + public bool? isAssistant { get; set; } public string? subscriptionPlanId { get; set; } public SubscriptionPlanDTO? subscriptionPlan { get; set; } - public int aiRequestsThisMonth { get; set; } + public int? aiRequestsThisMonth { get; set; } public string? aiUsageMonthKey { get; set; } + public long? storageQuotaBytes { get; set; } + public int? aiRequestsPerMonth { get; set; } + public bool? hasStats { get; set; } + public int? statsHistoryDays { get; set; } + public bool? hasAdvancedStats { get; set; } public List applicationInstanceDTOs { get; set; } } diff --git a/ManagerService/DTOs/SubscriptionPlanDTO.cs b/ManagerService/DTOs/SubscriptionPlanDTO.cs index c42511f..9f98aac 100644 --- a/ManagerService/DTOs/SubscriptionPlanDTO.cs +++ b/ManagerService/DTOs/SubscriptionPlanDTO.cs @@ -6,5 +6,8 @@ namespace ManagerService.DTOs public string name { get; set; } public long storageQuotaBytes { get; set; } public int aiRequestsPerMonth { get; set; } + public bool hasStats { get; set; } + public int statsHistoryDays { get; set; } + public bool hasAdvancedStats { get; set; } } } diff --git a/ManagerService/Data/AuditLog.cs b/ManagerService/Data/AuditLog.cs new file mode 100644 index 0000000..e6cf623 --- /dev/null +++ b/ManagerService/Data/AuditLog.cs @@ -0,0 +1,17 @@ +using System; + +namespace ManagerService.Data +{ + public class AuditLog + { + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string EntityType { get; set; } + public string EntityId { get; set; } + public string Action { get; set; } + public string? UserId { get; set; } + public string? InstanceId { get; set; } + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + public string? OldValues { get; set; } + public string? NewValues { get; set; } + } +} diff --git a/ManagerService/Data/Device.cs b/ManagerService/Data/Device.cs index c75d162..b90e3bc 100644 --- a/ManagerService/Data/Device.cs +++ b/ManagerService/Data/Device.cs @@ -71,6 +71,10 @@ namespace ManagerService.Data [Required] public string InstanceId { get; set; } + public string? AppVersion { get; set; } + + public DateTime? LastSeen { get; set; } + public DeviceDTO ToDTO() { return new DeviceDTO() diff --git a/ManagerService/Data/Instance.cs b/ManagerService/Data/Instance.cs index 3c42ce6..28db7f8 100644 --- a/ManagerService/Data/Instance.cs +++ b/ManagerService/Data/Instance.cs @@ -45,6 +45,16 @@ namespace ManagerService.Data public string AiUsageMonthKey { get; set; } = ""; + public long StorageQuotaBytes { get; set; } = 0; + + public int AiRequestsPerMonth { get; set; } = 0; + + public bool HasStats { get; set; } = false; + + public int StatsHistoryDays { get; set; } = 30; + + public bool HasAdvancedStats { get; set; } = false; + public InstanceDTO ToDTO(List applicationInstanceDTOs) { @@ -65,6 +75,11 @@ namespace ManagerService.Data subscriptionPlan = SubscriptionPlan?.ToDTO(), aiRequestsThisMonth = AiRequestsThisMonth, aiUsageMonthKey = AiUsageMonthKey, + storageQuotaBytes = StorageQuotaBytes, + aiRequestsPerMonth = AiRequestsPerMonth, + hasStats = HasStats, + statsHistoryDays = StatsHistoryDays, + hasAdvancedStats = HasAdvancedStats, applicationInstanceDTOs = applicationInstanceDTOs }; } @@ -74,15 +89,25 @@ namespace ManagerService.Data Name = instanceDTO.name; DateCreation = instanceDTO.dateCreation != null ? instanceDTO.dateCreation.Value : DateTime.Now.ToUniversalTime(); PinCode = instanceDTO.pinCode; - IsPushNotification = instanceDTO.isPushNotification; - IsStatistic = instanceDTO.isStatistic; - IsMobile = instanceDTO.isMobile; - IsTablet = instanceDTO.isTablet; - IsWeb = instanceDTO.isWeb; - IsVR = instanceDTO.isVR; - IsAssistant = instanceDTO.isAssistant; + IsPushNotification = instanceDTO.isPushNotification ?? false; + IsStatistic = instanceDTO.isStatistic ?? false; + IsMobile = instanceDTO.isMobile ?? false; + IsTablet = instanceDTO.isTablet ?? false; + IsWeb = instanceDTO.isWeb ?? false; + IsVR = instanceDTO.isVR ?? false; + IsAssistant = instanceDTO.isAssistant ?? false; if (instanceDTO.subscriptionPlanId != null) SubscriptionPlanId = instanceDTO.subscriptionPlanId; + if (instanceDTO.storageQuotaBytes.HasValue) + StorageQuotaBytes = instanceDTO.storageQuotaBytes.Value; + if (instanceDTO.aiRequestsPerMonth.HasValue) + AiRequestsPerMonth = instanceDTO.aiRequestsPerMonth.Value; + if (instanceDTO.hasStats.HasValue) + HasStats = instanceDTO.hasStats.Value; + if (instanceDTO.statsHistoryDays.HasValue) + StatsHistoryDays = instanceDTO.statsHistoryDays.Value; + if (instanceDTO.hasAdvancedStats.HasValue) + HasAdvancedStats = instanceDTO.hasAdvancedStats.Value; return this; } diff --git a/ManagerService/Data/MyInfoMateDbContext.cs b/ManagerService/Data/MyInfoMateDbContext.cs index cfe3323..f6943ab 100644 --- a/ManagerService/Data/MyInfoMateDbContext.cs +++ b/ManagerService/Data/MyInfoMateDbContext.cs @@ -1,16 +1,28 @@ 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 { - public MyInfoMateDbContext(DbContextOptions options) : base(options) { } + private readonly IHttpContextAccessor _httpContextAccessor; + + public MyInfoMateDbContext(DbContextOptions options, IHttpContextAccessor httpContextAccessor) + : base(options) + { + _httpContextAccessor = httpContextAccessor; + } public DbSet Instances { get; set; } public DbSet SubscriptionPlans { get; set; } @@ -49,6 +61,82 @@ namespace ManagerService.Data // 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) { @@ -59,6 +147,20 @@ namespace ManagerService.Data 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") @@ -240,6 +342,19 @@ namespace ManagerService.Data .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 @@ -248,20 +363,29 @@ namespace ManagerService.Data 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 = 100, + AiRequestsPerMonth = 500, + HasStats = true, + StatsHistoryDays = 30, + HasAdvancedStats = false, }, new SubscriptionPlan { Id = "plan-premium", Name = "Premium", StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB - AiRequestsPerMonth = 500, + AiRequestsPerMonth = 2000, + HasStats = true, + StatsHistoryDays = 0, + HasAdvancedStats = true, } ); } diff --git a/ManagerService/Data/SubscriptionPlan.cs b/ManagerService/Data/SubscriptionPlan.cs index 8ebafd2..a8e8c66 100644 --- a/ManagerService/Data/SubscriptionPlan.cs +++ b/ManagerService/Data/SubscriptionPlan.cs @@ -16,6 +16,15 @@ namespace ManagerService.Data public int AiRequestsPerMonth { get; set; } = 0; + /// Whether the plan includes any stats at all. + public bool HasStats { get; set; } = false; + + /// Max history in days for stats. 0 = unlimited (Premium). + public int StatsHistoryDays { get; set; } = 30; + + /// Whether the plan includes advanced stats (POI, quiz, game, articles, QR, etc.). + public bool HasAdvancedStats { get; set; } = false; + public SubscriptionPlanDTO ToDTO() { return new SubscriptionPlanDTO @@ -23,7 +32,10 @@ namespace ManagerService.Data id = Id, name = Name, storageQuotaBytes = StorageQuotaBytes, - aiRequestsPerMonth = AiRequestsPerMonth + aiRequestsPerMonth = AiRequestsPerMonth, + hasStats = HasStats, + statsHistoryDays = StatsHistoryDays, + hasAdvancedStats = HasAdvancedStats, }; } @@ -32,6 +44,9 @@ namespace ManagerService.Data Name = dto.name; StorageQuotaBytes = dto.storageQuotaBytes; AiRequestsPerMonth = dto.aiRequestsPerMonth; + HasStats = dto.hasStats; + StatsHistoryDays = dto.statsHistoryDays; + HasAdvancedStats = dto.hasAdvancedStats; return this; } } diff --git a/ManagerService/ManagerService.csproj b/ManagerService/ManagerService.csproj index 5f477c2..e49ccfe 100644 --- a/ManagerService/ManagerService.csproj +++ b/ManagerService/ManagerService.csproj @@ -25,6 +25,9 @@ + + + diff --git a/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.cs b/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.cs index 13b9745..84b2340 100644 --- a/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.cs +++ b/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.cs @@ -17,9 +17,9 @@ namespace ManagerService.Migrations columns: new[] { "Id", "AiRequestsPerMonth", "Name", "StorageQuotaBytes" }, values: new object[,] { - { "plan-premium", 500, "Premium", 53687091200L }, - { "plan-standard", 100, "Standard", 10737418240L }, - { "plan-starter", 0, "Starter", 1073741824L } + { "plan-premium", 2000, "Premium", 53687091200L }, + { "plan-standard", 500, "Standard", 10737418240L }, + { "plan-starter", 0, "Starter", 2147483648L } }); } diff --git a/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs b/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs new file mode 100644 index 0000000..ece8862 --- /dev/null +++ b/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.Designer.cs @@ -0,0 +1,1557 @@ +// +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; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260410122319_AddStatsHistoryToSubscriptionPlan")] + partial class AddStatsHistoryToSubscriptionPlan + { + /// + 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.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("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("DeviceId") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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.Property("WeightMasonryGrid") + .HasColumnType("integer"); + + 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("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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("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.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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.ToTable("Configurations"); + }); + + 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.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiRequestsThisMonth") + .HasColumnType("integer"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsStatistic") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .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("SubscriptionPlanId") + .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("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + 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("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("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("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("Description") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionGameId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionGameId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsHiddenInitially") + .HasColumnType("boolean"); + + b.Property("IsStepLocked") + .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("TriggerGeoPointId") + .HasColumnType("integer"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.HasIndex("TriggerGeoPointId"); + + 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("AiRequestsPerMonth") + .HasColumnType("integer"); + + b.Property("HasAdvancedStats") + .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", + AiRequestsPerMonth = 0, + HasAdvancedStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiRequestsPerMonth = 100, + HasAdvancedStats = false, + Name = "Standard", + StatsHistoryDays = 30, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiRequestsPerMonth = 500, + HasAdvancedStats = false, + Name = "Premium", + StatsHistoryDays = 30, + StorageQuotaBytes = 53687091200L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + 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.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>("ParcoursIds") + .HasColumnType("jsonb"); + + 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("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("MapResourceId") + .HasColumnType("text"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("MapResourceId"); + + 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.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.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame") + .WithMany() + .HasForeignKey("SectionGameId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany() + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionGame"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint") + .WithMany() + .HasForeignKey("TriggerGeoPointId"); + + b.Navigation("GuidedPath"); + + b.Navigation("TriggerGeoPoint"); + }); + + 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", "MapResource") + .WithMany() + .HasForeignKey("MapResourceId"); + + b.Navigation("MapResource"); + }); + + 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.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.cs b/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.cs new file mode 100644 index 0000000..95537a7 --- /dev/null +++ b/ManagerService/Migrations/20260410122319_AddStatsHistoryToSubscriptionPlan.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddStatsHistoryToSubscriptionPlan : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HasAdvancedStats", + table: "SubscriptionPlans", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "StatsHistoryDays", + table: "SubscriptionPlans", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AppVersion", + table: "Devices", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "LastSeen", + table: "Devices", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.CreateTable( + name: "AuditLogs", + columns: table => new + { + Id = table.Column(type: "text", nullable: false), + EntityType = table.Column(type: "text", nullable: true), + EntityId = table.Column(type: "text", nullable: true), + Action = table.Column(type: "text", nullable: true), + UserId = table.Column(type: "text", nullable: true), + InstanceId = table.Column(type: "text", nullable: true), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + OldValues = table.Column(type: "text", nullable: true), + NewValues = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AuditLogs", x => x.Id); + }); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-premium", + columns: new[] { "HasAdvancedStats", "StatsHistoryDays" }, + values: new object[] { false, 30 }); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-standard", + columns: new[] { "HasAdvancedStats", "StatsHistoryDays" }, + values: new object[] { false, 30 }); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-starter", + columns: new[] { "HasAdvancedStats", "StatsHistoryDays" }, + values: new object[] { false, 30 }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AuditLogs"); + + migrationBuilder.DropColumn( + name: "HasAdvancedStats", + table: "SubscriptionPlans"); + + migrationBuilder.DropColumn( + name: "StatsHistoryDays", + table: "SubscriptionPlans"); + + migrationBuilder.DropColumn( + name: "AppVersion", + table: "Devices"); + + migrationBuilder.DropColumn( + name: "LastSeen", + table: "Devices"); + } + } +} diff --git a/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs b/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs new file mode 100644 index 0000000..8d1e277 --- /dev/null +++ b/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.Designer.cs @@ -0,0 +1,1557 @@ +// +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; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260410122549_FixPremiumStatsSettings")] + partial class FixPremiumStatsSettings + { + /// + 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.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("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("DeviceId") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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.Property("WeightMasonryGrid") + .HasColumnType("integer"); + + 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("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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("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.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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.ToTable("Configurations"); + }); + + 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.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiRequestsThisMonth") + .HasColumnType("integer"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsStatistic") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .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("SubscriptionPlanId") + .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("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + 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("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("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("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("Description") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionGameId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionGameId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsHiddenInitially") + .HasColumnType("boolean"); + + b.Property("IsStepLocked") + .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("TriggerGeoPointId") + .HasColumnType("integer"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.HasIndex("TriggerGeoPointId"); + + 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("AiRequestsPerMonth") + .HasColumnType("integer"); + + b.Property("HasAdvancedStats") + .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", + AiRequestsPerMonth = 0, + HasAdvancedStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiRequestsPerMonth = 100, + HasAdvancedStats = false, + Name = "Standard", + StatsHistoryDays = 30, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiRequestsPerMonth = 500, + HasAdvancedStats = true, + Name = "Premium", + StatsHistoryDays = 0, + StorageQuotaBytes = 53687091200L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + 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.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>("ParcoursIds") + .HasColumnType("jsonb"); + + 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("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("MapResourceId") + .HasColumnType("text"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("MapResourceId"); + + 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.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.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame") + .WithMany() + .HasForeignKey("SectionGameId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany() + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionGame"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint") + .WithMany() + .HasForeignKey("TriggerGeoPointId"); + + b.Navigation("GuidedPath"); + + b.Navigation("TriggerGeoPoint"); + }); + + 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", "MapResource") + .WithMany() + .HasForeignKey("MapResourceId"); + + b.Navigation("MapResource"); + }); + + 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.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.cs b/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.cs new file mode 100644 index 0000000..6aea04f --- /dev/null +++ b/ManagerService/Migrations/20260410122549_FixPremiumStatsSettings.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class FixPremiumStatsSettings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-premium", + columns: new[] { "HasAdvancedStats", "StatsHistoryDays" }, + values: new object[] { true, 0 }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-premium", + columns: new[] { "HasAdvancedStats", "StatsHistoryDays" }, + values: new object[] { false, 30 }); + } + } +} diff --git a/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs b/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs new file mode 100644 index 0000000..7b31b0d --- /dev/null +++ b/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.Designer.cs @@ -0,0 +1,1578 @@ +// +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; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260410123835_AddQuotaFieldsToInstance")] + partial class AddQuotaFieldsToInstance + { + /// + 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.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("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("DeviceId") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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.Property("WeightMasonryGrid") + .HasColumnType("integer"); + + 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("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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("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.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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.ToTable("Configurations"); + }); + + 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.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiRequestsPerMonth") + .HasColumnType("integer"); + + b.Property("AiRequestsThisMonth") + .HasColumnType("integer"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsStatistic") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .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("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.Property("SubscriptionPlanId") + .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("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + 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("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("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("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("Description") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionGameId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionGameId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsHiddenInitially") + .HasColumnType("boolean"); + + b.Property("IsStepLocked") + .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("TriggerGeoPointId") + .HasColumnType("integer"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.HasIndex("TriggerGeoPointId"); + + 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("AiRequestsPerMonth") + .HasColumnType("integer"); + + 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", + AiRequestsPerMonth = 0, + HasAdvancedStats = false, + HasStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiRequestsPerMonth = 500, + HasAdvancedStats = false, + HasStats = true, + Name = "Standard", + StatsHistoryDays = 30, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiRequestsPerMonth = 2000, + HasAdvancedStats = true, + HasStats = true, + Name = "Premium", + StatsHistoryDays = 0, + StorageQuotaBytes = 53687091200L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + 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.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>("ParcoursIds") + .HasColumnType("jsonb"); + + 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("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("MapResourceId") + .HasColumnType("text"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("MapResourceId"); + + 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.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.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame") + .WithMany() + .HasForeignKey("SectionGameId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany() + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionGame"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint") + .WithMany() + .HasForeignKey("TriggerGeoPointId"); + + b.Navigation("GuidedPath"); + + b.Navigation("TriggerGeoPoint"); + }); + + 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", "MapResource") + .WithMany() + .HasForeignKey("MapResourceId"); + + b.Navigation("MapResource"); + }); + + 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.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.cs b/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.cs new file mode 100644 index 0000000..73b1a1a --- /dev/null +++ b/ManagerService/Migrations/20260410123835_AddQuotaFieldsToInstance.cs @@ -0,0 +1,119 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class AddQuotaFieldsToInstance : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HasStats", + table: "SubscriptionPlans", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "AiRequestsPerMonth", + table: "Instances", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "HasAdvancedStats", + table: "Instances", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "HasStats", + table: "Instances", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "StatsHistoryDays", + table: "Instances", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "StorageQuotaBytes", + table: "Instances", + type: "bigint", + nullable: false, + defaultValue: 0L); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-premium", + columns: new[] { "AiRequestsPerMonth", "HasStats" }, + values: new object[] { 2000, true }); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-standard", + columns: new[] { "AiRequestsPerMonth", "HasStats" }, + values: new object[] { 500, true }); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-starter", + column: "HasStats", + value: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HasStats", + table: "SubscriptionPlans"); + + migrationBuilder.DropColumn( + name: "AiRequestsPerMonth", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "HasAdvancedStats", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "HasStats", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "StatsHistoryDays", + table: "Instances"); + + migrationBuilder.DropColumn( + name: "StorageQuotaBytes", + table: "Instances"); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-premium", + column: "AiRequestsPerMonth", + value: 500); + + migrationBuilder.UpdateData( + table: "SubscriptionPlans", + keyColumn: "Id", + keyValue: "plan-standard", + column: "AiRequestsPerMonth", + value: 100); + } + } +} diff --git a/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs b/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs new file mode 100644 index 0000000..04b3267 --- /dev/null +++ b/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.Designer.cs @@ -0,0 +1,1578 @@ +// +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; + +#nullable disable + +namespace ManagerService.Migrations +{ + [DbContext(typeof(MyInfoMateDbContext))] + [Migration("20260410124619_FixInstanceQuotaColumnsWritable")] + partial class FixInstanceQuotaColumnsWritable + { + /// + 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.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("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("DeviceId") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDate") + .HasColumnType("boolean"); + + b.Property("IsHour") + .HasColumnType("boolean"); + + b.Property("IsSectionImageBackground") + .HasColumnType("boolean"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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.Property("WeightMasonryGrid") + .HasColumnType("integer"); + + 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("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.PrimitiveCollection>("Languages") + .HasColumnType("text[]"); + + b.Property("LayoutMainPage") + .HasColumnType("integer"); + + 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("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.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("ManagerService.Data.Configuration", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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.ToTable("Configurations"); + }); + + 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.ToTable("Devices"); + }); + + modelBuilder.Entity("ManagerService.Data.Instance", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AiRequestsPerMonth") + .HasColumnType("integer"); + + b.Property("AiRequestsThisMonth") + .HasColumnType("integer"); + + b.Property("AiUsageMonthKey") + .HasColumnType("text"); + + b.Property("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + + b.Property("IsAssistant") + .HasColumnType("boolean"); + + b.Property("IsMobile") + .HasColumnType("boolean"); + + b.Property("IsPushNotification") + .HasColumnType("boolean"); + + b.Property("IsStatistic") + .HasColumnType("boolean"); + + b.Property("IsTablet") + .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("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + + b.Property("SubscriptionPlanId") + .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("DateCreation") + .HasColumnType("timestamp with time zone"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("Url") + .HasColumnType("text"); + + b.HasKey("Id"); + + 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("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("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("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("Description") + .HasColumnType("jsonb"); + + b.Property("HideNextStepsUntilComplete") + .HasColumnType("boolean"); + + b.Property("InstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsLinear") + .HasColumnType("boolean"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("RequireSuccessToAdvance") + .HasColumnType("boolean"); + + b.Property("SectionEventId") + .HasColumnType("text"); + + b.Property("SectionGameId") + .HasColumnType("text"); + + b.Property("SectionMapId") + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("jsonb"); + + b.HasKey("Id"); + + b.HasIndex("SectionEventId"); + + b.HasIndex("SectionGameId"); + + b.HasIndex("SectionMapId"); + + b.ToTable("GuidedPaths"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Description") + .HasColumnType("jsonb"); + + b.Property("Geometry") + .HasColumnType("geometry"); + + b.Property("GuidedPathId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ImageUrl") + .HasColumnType("text"); + + b.Property("IsHiddenInitially") + .HasColumnType("boolean"); + + b.Property("IsStepLocked") + .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("TriggerGeoPointId") + .HasColumnType("integer"); + + b.Property("ZoneRadiusMeters") + .HasColumnType("double precision"); + + b.HasKey("Id"); + + b.HasIndex("GuidedPathId"); + + b.HasIndex("TriggerGeoPointId"); + + 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("AiRequestsPerMonth") + .HasColumnType("integer"); + + 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", + AiRequestsPerMonth = 0, + HasAdvancedStats = false, + HasStats = false, + Name = "Starter", + StatsHistoryDays = 30, + StorageQuotaBytes = 1073741824L + }, + new + { + Id = "plan-standard", + AiRequestsPerMonth = 500, + HasAdvancedStats = false, + HasStats = true, + Name = "Standard", + StatsHistoryDays = 30, + StorageQuotaBytes = 10737418240L + }, + new + { + Id = "plan-premium", + AiRequestsPerMonth = 2000, + HasAdvancedStats = true, + HasStats = true, + Name = "Premium", + StatsHistoryDays = 0, + StorageQuotaBytes = 53687091200L + }); + }); + + modelBuilder.Entity("ManagerService.Data.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("DateCreation") + .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("Role") + .HasColumnType("integer"); + + b.Property("Token") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + 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.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>("ParcoursIds") + .HasColumnType("jsonb"); + + 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("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("MapResourceId") + .HasColumnType("text"); + + b.Property("MapTypeMapbox") + .HasColumnType("integer"); + + b.Property("MapZoom") + .HasColumnType("integer"); + + b.HasIndex("MapResourceId"); + + 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.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.SubSection.SectionEvent", "SectionEvent") + .WithMany() + .HasForeignKey("SectionEventId"); + + b.HasOne("ManagerService.Data.SubSection.SectionGame", "SectionGame") + .WithMany() + .HasForeignKey("SectionGameId"); + + b.HasOne("ManagerService.Data.SubSection.SectionMap", "SectionMap") + .WithMany() + .HasForeignKey("SectionMapId"); + + b.Navigation("SectionEvent"); + + b.Navigation("SectionGame"); + + b.Navigation("SectionMap"); + }); + + modelBuilder.Entity("ManagerService.Data.SubSection.GuidedStep", b => + { + b.HasOne("ManagerService.Data.SubSection.GuidedPath", "GuidedPath") + .WithMany("Steps") + .HasForeignKey("GuidedPathId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ManagerService.Data.SubSection.GeoPoint", "TriggerGeoPoint") + .WithMany() + .HasForeignKey("TriggerGeoPointId"); + + b.Navigation("GuidedPath"); + + b.Navigation("TriggerGeoPoint"); + }); + + 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", "MapResource") + .WithMany() + .HasForeignKey("MapResourceId"); + + b.Navigation("MapResource"); + }); + + 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.SectionQuiz", b => + { + b.Navigation("QuizQuestions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.cs b/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.cs new file mode 100644 index 0000000..a72bdc5 --- /dev/null +++ b/ManagerService/Migrations/20260410124619_FixInstanceQuotaColumnsWritable.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ManagerService.Migrations +{ + /// + public partial class FixInstanceQuotaColumnsWritable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs index c140af9..23e8cad 100644 --- a/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs +++ b/ManagerService/Migrations/MyInfoMateDbContextModelSnapshot.cs @@ -180,6 +180,40 @@ namespace ManagerService.Migrations 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.ToTable("AuditLogs"); + }); + modelBuilder.Entity("ManagerService.Data.Configuration", b => { b.Property("Id") @@ -243,6 +277,9 @@ namespace ManagerService.Migrations b.Property("Id") .HasColumnType("text"); + b.Property("AppVersion") + .HasColumnType("text"); + b.Property("BatteryLevel") .HasColumnType("text"); @@ -281,6 +318,9 @@ namespace ManagerService.Migrations b.Property("LastConnectionLevel") .HasColumnType("timestamp with time zone"); + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + b.Property("Name") .HasColumnType("text"); @@ -296,6 +336,9 @@ namespace ManagerService.Migrations b.Property("Id") .HasColumnType("text"); + b.Property("AiRequestsPerMonth") + .HasColumnType("integer"); + b.Property("AiRequestsThisMonth") .HasColumnType("integer"); @@ -305,6 +348,12 @@ namespace ManagerService.Migrations b.Property("DateCreation") .HasColumnType("timestamp with time zone"); + b.Property("HasAdvancedStats") + .HasColumnType("boolean"); + + b.Property("HasStats") + .HasColumnType("boolean"); + b.Property("IsAssistant") .HasColumnType("boolean"); @@ -333,6 +382,12 @@ namespace ManagerService.Migrations b.Property("PinCode") .HasColumnType("text"); + b.Property("StatsHistoryDays") + .HasColumnType("integer"); + + b.Property("StorageQuotaBytes") + .HasColumnType("bigint"); + b.Property("SubscriptionPlanId") .HasColumnType("text"); @@ -879,10 +934,19 @@ namespace ManagerService.Migrations b.Property("AiRequestsPerMonth") .HasColumnType("integer"); + 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"); @@ -895,21 +959,30 @@ namespace ManagerService.Migrations { Id = "plan-starter", AiRequestsPerMonth = 0, + HasAdvancedStats = false, + HasStats = false, Name = "Starter", + StatsHistoryDays = 30, StorageQuotaBytes = 1073741824L }, new { Id = "plan-standard", - AiRequestsPerMonth = 100, + AiRequestsPerMonth = 500, + HasAdvancedStats = false, + HasStats = true, Name = "Standard", + StatsHistoryDays = 30, StorageQuotaBytes = 10737418240L }, new { Id = "plan-premium", - AiRequestsPerMonth = 500, + AiRequestsPerMonth = 2000, + HasAdvancedStats = true, + HasStats = true, Name = "Premium", + StatsHistoryDays = 0, StorageQuotaBytes = 53687091200L }); }); diff --git a/ManagerService/Program.cs b/ManagerService/Program.cs index 4dc4131..d8cec87 100644 --- a/ManagerService/Program.cs +++ b/ManagerService/Program.cs @@ -1,11 +1,6 @@ using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using Serilog; namespace ManagerService { @@ -13,11 +8,30 @@ namespace ManagerService { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateBootstrapLogger(); + + try + { + CreateHostBuilder(args).Build().Run(); + } + catch (System.Exception ex) + { + Log.Fatal(ex, "Application terminated unexpectedly"); + } + finally + { + Log.CloseAndFlush(); + } } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext()) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/ManagerService/Services/AssistantService.cs b/ManagerService/Services/AssistantService.cs index 3b3b533..dd461f4 100644 --- a/ManagerService/Services/AssistantService.cs +++ b/ManagerService/Services/AssistantService.cs @@ -13,7 +13,7 @@ using ManagerService.Data.SubSection; namespace ManagerService.Services { - public class AssistantService + public class AssistantService : IAssistantService { private readonly IChatClient _chatClient; private readonly MyInfoMateDbContext _context; @@ -26,6 +26,40 @@ namespace ManagerService.Services _context = context; } + public async Task TranslateAsync(AiTranslateRequest request) + { + var targetList = string.Join(", ", request.TargetLangs); + var exampleJson = "{" + string.Join(", ", request.TargetLangs.Select(l => $"\"{l}\": \"...\"")) + "}"; + var prompt = $""" + Tu es un traducteur professionnel. Les codes de langue utilisés sont : FR=français, NL=néerlandais, EN=anglais, DE=allemand, IT=italien, ES=espagnol, PL=polonais, CN=chinois, AR=arabe, UK=ukrainien. + Traduis le texte HTML suivant de la langue "{request.SourceLang}" vers ces langues : {targetList}. + Conserve exactement le formatage HTML (balises, attributs, structure). Ne traduis pas le contenu des attributs HTML. + Réponds UNIQUEMENT avec un objet JSON valide, sans markdown, sans explication, au format : + {exampleJson} + + Texte à traduire : + {request.Text} + """; + + var messages = new List + { + new ChatMessage(ChatRole.User, prompt) + }; + + var response = await _chatClient.GetResponseAsync(messages); + var json = response.Text?.Trim() ?? "{}"; + + // Nettoyer les éventuels blocs markdown ```json ... ``` + if (json.StartsWith("```")) + { + json = System.Text.RegularExpressions.Regex.Replace(json, @"```[a-z]*\n?", "").Trim(); + json = json.TrimEnd('`').Trim(); + } + + var translations = Newtonsoft.Json.JsonConvert.DeserializeObject>(json) ?? new(); + return new AiTranslateResponse { Translations = translations }; + } + private static string StripHtml(string? html) => string.IsNullOrEmpty(html) ? "" : System.Text.RegularExpressions.Regex.Replace(html, "<[^>]+>", "").Trim(); diff --git a/ManagerService/Services/IAssistantService.cs b/ManagerService/Services/IAssistantService.cs new file mode 100644 index 0000000..90ca495 --- /dev/null +++ b/ManagerService/Services/IAssistantService.cs @@ -0,0 +1,11 @@ +using ManagerService.DTOs; +using System.Threading.Tasks; + +namespace ManagerService.Services +{ + public interface IAssistantService + { + Task ChatAsync(AiChatRequest request); + Task TranslateAsync(AiTranslateRequest request); + } +} diff --git a/ManagerService/Startup.cs b/ManagerService/Startup.cs index 78c02f2..6fa02c8 100644 --- a/ManagerService/Startup.cs +++ b/ManagerService/Startup.cs @@ -46,6 +46,7 @@ using System.Text; using System.Text.Json.Serialization; using System.Threading.Tasks; using ManagerService.Services; +using Serilog; namespace ManagerService { @@ -157,6 +158,7 @@ namespace ManagerService #if RELEASE //services.AddMqttClientHostedService(); #endif + services.AddHttpContextAccessor(); services.AddScoped(typeof(ProfileLogic)); services.AddScoped(); services.AddScoped(); @@ -182,7 +184,7 @@ namespace ManagerService .AsBuilder() .UseFunctionInvocation() .Build()); - services.AddScoped(); + services.AddScoped(); // Push Notifications var firebaseCredentialsPath = Configuration["Firebase:CredentialsPath"]; @@ -215,9 +217,12 @@ namespace ManagerService services.AddDbContext(options => options.UseNpgsql(dataSource, o => o.UseNetTopologySuite()) - .EnableSensitiveDataLogging() // montre les valeurs des param�tres + .EnableSensitiveDataLogging() .LogTo(Console.WriteLine, LogLevel.Information) ); + + services.AddHealthChecks() + .AddNpgSql(connectionString); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -233,6 +238,7 @@ namespace ManagerService } app.UseExceptionHandler(HandleError); + app.UseSerilogRequestLogging(); //app.UseHttpsRedirection(); @@ -280,6 +286,7 @@ namespace ManagerService app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapHealthChecks("/health"); }); app.UseOpenApi(); diff --git a/ManagerService/appsettings.Development.json b/ManagerService/appsettings.Development.json index 898e5f8..740e796 100644 --- a/ManagerService/appsettings.Development.json +++ b/ManagerService/appsettings.Development.json @@ -4,8 +4,8 @@ //"TabletDb": "mongodb://admin:MioTech4ever!@localhost:27017", //PROD - Thomas //"TabletDb": "mongodb://admin:MioTech4ever!@192.168.31.140:27017" //PROD - Thomas //"TabletDb": "mongodb://admin:mdlf2021!@localhost:27017" //PROD MDLF - "TabletDb": "mongodb://admin:MyMuseum2022!@51.77.222.154:27017", //PROD MyMuseum - //"TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017" //PROD MyInfoMate + //"TabletDb": "mongodb://admin:MyMuseum2022!@51.77.222.154:27017", //PROD MyMuseum + "TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017", //PROD MyInfoMate "PostgresConnection": "Host=localhost;Database=my_info_mate;Username=mym;Password=mym" }, "BrokerHostSettings": { diff --git a/ManagerService/appsettings.PreProduction.json b/ManagerService/appsettings.PreProduction.json index 23a6162..240596f 100644 --- a/ManagerService/appsettings.PreProduction.json +++ b/ManagerService/appsettings.PreProduction.json @@ -17,11 +17,17 @@ "UserName": "user1", //admin "Password": "MyInfoMate2023!" //mdlf2021! }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "Serilog": { + "WriteTo": [ + { "Name": "Console" }, + { + "Name": "Telegram", + "Args": { + "token": "8427411434:AAEp0NnlhKkKZz07P0WftS2KOuRmoA99vKs", + "chatId": "44670399", + "restrictedToMinimumLevel": "Error" + } + } + ] } } diff --git a/ManagerService/appsettings.json b/ManagerService/appsettings.json index 0e6c391..abca84b 100644 --- a/ManagerService/appsettings.json +++ b/ManagerService/appsettings.json @@ -8,12 +8,19 @@ //"TabletDb": "mongodb://admin:MyInfoMate2023!@135.125.232.116:27017" //PROD MyInfoMate "PostgresConnection": "Host=localhost;Database=my_info_mate;Username=mym;Password=mym" },*/ - "Logging": { - "LogLevel": { + "Serilog": { + "MinimumLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ "FromLogContext" ] }, "AllowedHosts": "*", "Tokens": { diff --git a/docker-compose-myinfomate.yml b/docker-compose-myinfomate.yml new file mode 100644 index 0000000..b277876 --- /dev/null +++ b/docker-compose-myinfomate.yml @@ -0,0 +1,210 @@ +version: "3.3" + +services: + ################################################ + #### Traefik Proxy Setup ##### + ############################################### + traefik: + image: traefik:v2.8 + restart: always + container_name: traefik + ports: + - "80:80" # <== http + - "8080:8080" # <== :8080 is where the dashboard runs on + - "443:443" # <== https + command: + #### These are the CLI commands that will configure Traefik and tell it how to work! #### + ## API Settings - https://docs.traefik.io/operations/api/, endpoints - https://docs.traefik.io/operations/api/#endpoints ## + - --api.insecure=true # <== Enabling insecure api, NOT RECOMMENDED FOR PRODUCTION + - --api.dashboard=true # <== Enabling the dashboard to view services, middlewares, routers, etc... + - --api.debug=true # <== Enabling additional endpoints for debugging and profiling + ## Log Settings (options: ERROR, DEBUG, PANIC, FATAL, WARN, INFO) - https://docs.traefik.io/observability/logs/ ## + - --log.level=ERROR # <== Setting the level of the logs from traefik + ## Provider Settings - https://docs.traefik.io/providers/docker/#provider-configuration ## + - --providers.docker=true # <== Enabling docker as the provider for traefik + - --providers.docker.exposedbydefault=false # <== Don't expose every container to traefik, only expose enabled ones + - --providers.file.filename=/dynamic.yaml # <== Referring to a dynamic configuration file + - --providers.docker.network=web # <== Operate on the docker network named web + ## Entrypoints Settings - https://docs.traefik.io/routing/entrypoints/#configuration ## + - --entrypoints.web.address=:80 # <== Defining an entrypoint for port :80 named web + - --entrypoints.web-secured.address=:443 # <== Defining an entrypoint for https on port :443 named web-secured + ## Certificate Settings (Let's Encrypt) - https://docs.traefik.io/https/acme/#configuration-examples ## + - --certificatesresolvers.mytlschallenge.acme.tlschallenge=true # <== Enable TLS-ALPN-01 to generate and renew ACME certs + - --certificatesresolvers.mytlschallenge.acme.email=fransolet.thomas@gmail.com # <== Setting email for certs + - --certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json # <== Defining acme file to store cert information + volumes: + - ./letsencrypt:/letsencrypt # <== Volume for certs (TLS) + - /var/run/docker.sock:/var/run/docker.sock # <== Volume for docker admin + - ./dynamic.yaml:/dynamic.yaml # <== Volume for dynamic conf file, **ref: line 27 + networks: + - web # <== Placing traefik on the network named web, to access containers on this network + labels: + #### Labels define the behavior and rules of the traefik proxy for this container #### + - "traefik.enable=true" # <== Enable traefik on itself to view dashboard and assign subdomain to view it + - "traefik.http.routers.api.rule=Host(`monitor.myinfomate.be`)" # <== Setting the domain for the dashboard + - "traefik.http.routers.api.service=api@internal" # <== Enabling the api to be a service to access + - "traefik.http.routers.api.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.api-secured.rule=Host(`monitor.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.api-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.api-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + - "traefik.http.routers.api-secured.service=api@internal" # <== Enabling the api to be a service to access + + ################################################ + #### Site Setup Container ##### + ############################################## + myinfomate-landing: + container_name: "myinfomate-landing" + image: registry.unov.be/myinfomate/landing:latest + restart: always + networks: + - web + labels: + - "traefik.enable=true" + - "traefik.http.routers.landing.rule=Host(`myinfomate.be`)" + - "traefik.http.routers.landing.entrypoints=web" + - "traefik.http.routers.landing.middlewares=redirect@file" + - "traefik.http.routers.landing-secured.rule=Host(`myinfomate.be`)" + - "traefik.http.routers.landing-secured.entrypoints=web-secured" + - "traefik.http.routers.landing-secured.tls.certresolver=mytlschallenge" + + managerService: + container_name: "manager-service" + image: registry.unov.be/managerservice:version-2.0.0 + networks: + - web + - backend + #ports: + # - 5005:5005 + volumes: + # - /etc/managerservice + - ~/apps/manager:/app/service-data + restart: always + labels: + - "traefik.enable=true" + - "traefik.http.routers.manager-service.rule=Host(`api.myinfomate.be`)" # <== Your Domain Name goes here for the http rule + - "traefik.http.routers.manager-service.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 + - "traefik.http.routers.manager-service.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.manager-service-secured.rule=Host(`api.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.manager-service-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.manager-service-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + command: /bin/sh -c "sudo chmod -R 777 /root/service-data/configurations /root/service-data/resources" + managerWeb: + container_name: "manager-web" + image: registry.unov.be/mymuseum/manager:version-2.0.0 + networks: + - web + volumes: + - /etc/managerweb + restart: always + labels: + - "traefik.enable=true" + - "traefik.http.routers.manager-web.rule=Host(`manager.myinfomate.be`, `visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name goes here for the http rule + - "traefik.http.routers.manager-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 + - "traefik.http.routers.manager-web.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.manager-web-secured.rule=Host(`manager.myinfomate.be`, `visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.manager-web-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.manager-web-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + - "traefik.http.routers.manager-web-fsh.rule=Host(`visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name goes here for the http rule + - "traefik.http.routers.manager-web-fsh.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 + - "traefik.http.routers.manager-web-fsh.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.manager-web-fsh-secured.rule=Host(`visitnamur.myinfomate.be`, `fortsaintheribert.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.manager-web-fsh-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.manager-web-fsh-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + + demoWeb: + container_name: "demo-web" + image: registry.unov.be/mymuseum/demo:latest + networks: + - web + volumes: + - /etc/demoweb + restart: always + labels: + - "traefik.enable=true" + - "traefik.http.routers.demo-web.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name goes here for the http rule + - "traefik.http.routers.demo-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 + - "traefik.http.routers.demo-web.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.demo-web-secured.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.demo-web-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.demo-web-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + - "traefik.http.routers.demo-web-fsh.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name goes here for the http rule + - "traefik.http.routers.demo-web-fsh.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 + - "traefik.http.routers.demo-web-fsh.middlewares=redirect@file" # <== This is a middleware to redirect to https + - "traefik.http.routers.demo-web-fsh-secured.rule=Host(`demo.myinfomate.be`)" # <== Your Domain Name for the https rule + - "traefik.http.routers.demo-web-fsh-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 + - "traefik.http.routers.demo-web-fsh-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + + mqtt: + container_name: mqtt + image: eclipse-mosquitto + networks: + - web + ports: + - "1883:1883" #default mqtt port + - "9001:9001" #default mqtt port for websockets + volumes: + - /etc/mqtt/config:/mosquitto/config:rw + - /etc/mqtt/data:/mosquitto/data:rw + - /etc/mqtt/log:/mosquitto/log:rw + restart: always + labels: + - "traefik.enable=true" + - "traefik.docker.network=mqtt" + - "traefik.tcp.services.mqtt.loadbalancer.server.port=1883" + - "traefik.tcp.services.mqtt_websocket.loadbalancer.server.port=9001" + - "traefik.tcp.routers.tcpr_mqtt.entrypoints=mqtt" + - "traefik.tcp.routers.tcpr_mqtt.rule=HostSNI(`myinfomate.be`)" + - "traefik.tcp.routers.tcpr_mqtt.service=mqtt" + - "traefik.tcp.routers.tcpr_mqtt_websocket.entrypoints=websocket" + - "traefik.tcp.routers.tcpr_mqtt_websocket.rule=HostSNI(`myinfomate.be`)" + - "traefik.tcp.routers.tcpr_mqtt_websocket.service=mqtt_websocket" + + + ################################################ + #### DB Container not on traefik ##### + ############################################## + #db: + # image: mysql:8.0 + # container_name: db + # restart: unless-stopped + # command: '--default-authentication-plugin=mysql_native_password' + # env_file: .env + # environment: + # - MYSQL_DATABASE=wordpress + # volumes: + # - dbdata:/var/lib/mysql + # networks: + # - backend + + mongo: + image: mongo + container_name: "mongodb" + ports: + - 27017:27017 + volumes: + - ~/apps/mongo:/data/db + restart: always + labels: + - "traefik.enable=true" + - "traefik.tcp.routers.mongodb.rule=HostSNI(`myinfomate.be`)" + - "traefik.tcp.routers.mongo.entrypoints=mongo" + - "traefik.tcp.routers.mongo.tls=true" + - "traefik.tcp.services.mongo.loadbalancer.server.port=27017" + environment: + MONGO_INITDB_ROOT_USERNAME: $MONGODB_USERNAME + MONGO_INITDB_ROOT_PASSWORD: $MONGODB_PASSWORD + +networks: + web: + external: true + backend: + external: false + +volumes: + #wordpress: + # external: true + #dbdata: + db: + external: true + mongo: + external: true diff --git a/docker-compose.yml b/docker-compose.yml index 473de7c..2e0e16a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,31 +52,20 @@ services: ################################################ #### Site Setup Container ##### ############################################## - wordpress: # <== we aren't going to open :80 here because traefik is going to serve this on entrypoint 'web' - image: wordpress - depends_on: - - db + myinfomate-landing: + container_name: "myinfomate-landing" + image: registry.unov.be/myinfomate/landing:latest restart: always - container_name: wordpress - environment: - - WORDPRESS_DB_HOST=db:3306 - - WORDPRESS_DB_USER=$MYSQL_USER - - WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD - - WORDPRESS_DB_NAME=wordpress - volumes: - - wordpress:/var/www/html networks: - web - - backend labels: - #### Labels define the behavior and rules of the traefik proxy for this container #### - - "traefik.enable=true" # <== Enable traefik to proxy this container - - "traefik.http.routers.nginx-web.rule=Host(`mymuseum.be`)" # <== Your Domain Name goes here for the http rule - - "traefik.http.routers.nginx-web.entrypoints=web" # <== Defining the entrypoint for http, **ref: line 30 - - "traefik.http.routers.nginx-web.middlewares=redirect@file" # <== This is a middleware to redirect to https - - "traefik.http.routers.nginx-secured.rule=Host(`mymuseum.be`)" # <== Your Domain Name for the https rule - - "traefik.http.routers.nginx-secured.entrypoints=web-secured" # <== Defining entrypoint for https, **ref: line 31 - - "traefik.http.routers.nginx-secured.tls.certresolver=mytlschallenge" # <== Defining certsresolvers for https + - "traefik.enable=true" + - "traefik.http.routers.landing.rule=Host(`myinfomate.be`)" + - "traefik.http.routers.landing.entrypoints=web" + - "traefik.http.routers.landing.middlewares=redirect@file" + - "traefik.http.routers.landing-secured.rule=Host(`myinfomate.be`)" + - "traefik.http.routers.landing-secured.entrypoints=web-secured" + - "traefik.http.routers.landing-secured.tls.certresolver=mytlschallenge" managerService: container_name: "manager-service" @@ -119,19 +108,6 @@ services: ################################################ #### DB Container not on traefik ##### ############################################## - db: - image: mysql:8.0 - container_name: db - restart: unless-stopped - command: '--default-authentication-plugin=mysql_native_password' - env_file: .env - environment: - - MYSQL_DATABASE=wordpress - volumes: - - dbdata:/var/lib/mysql - networks: - - backend - mongo: image: mongo container_name: "mongodb" @@ -155,7 +131,7 @@ networks: external: true backend: external: false - + volumes: wordpress: external: true