diff --git a/ManagerService/Controllers/MigrationController.cs b/ManagerService/Controllers/MigrationController.cs
new file mode 100644
index 0000000..2c1ebdf
--- /dev/null
+++ b/ManagerService/Controllers/MigrationController.cs
@@ -0,0 +1,803 @@
+using Manager.DTOs;
+using Manager.Services;
+using ManagerService.Data;
+using ManagerService.Data.SubSection;
+using ManagerService.DTOs;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using NetTopologySuite.Geometries;
+using NSwag.Annotations;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace ManagerService.Controllers
+{
+ [Authorize(Policy = ManagerService.Service.Security.Policies.SuperAdmin)]
+ [ApiController, Route("api/[controller]")]
+ [OpenApiTag("Migration", Description = "MongoDB → PostgreSQL data migration")]
+ public class MigrationController : ControllerBase
+ {
+ private readonly MyInfoMateDbContext _db;
+ private readonly InstanceDatabaseService _instanceSvc;
+ private readonly ConfigurationDatabaseService _configSvc;
+ private readonly SectionDatabaseService _sectionSvc;
+ private readonly ResourceDatabaseService _resourceSvc;
+ private readonly UserDatabaseService _userSvc;
+ private readonly DeviceDatabaseService _deviceSvc;
+
+ public MigrationController(
+ MyInfoMateDbContext db,
+ InstanceDatabaseService instanceSvc,
+ ConfigurationDatabaseService configSvc,
+ SectionDatabaseService sectionSvc,
+ ResourceDatabaseService resourceSvc,
+ UserDatabaseService userSvc,
+ DeviceDatabaseService deviceSvc)
+ {
+ _db = db;
+ _instanceSvc = instanceSvc;
+ _configSvc = configSvc;
+ _sectionSvc = sectionSvc;
+ _resourceSvc = resourceSvc;
+ _userSvc = userSvc;
+ _deviceSvc = deviceSvc;
+ }
+
+ ///
+ /// Run (or simulate) the MongoDB → PostgreSQL migration.
+ /// Use dryRun=true to validate without writing anything to the database.
+ /// Use instanceId to restrict to a single instance.
+ ///
+ [HttpPost("run")]
+ [ProducesResponseType(typeof(MigrationReportDTO), 200)]
+ [ProducesResponseType(typeof(string), 500)]
+ public async Task Run(
+ [FromQuery] bool dryRun = false,
+ [FromQuery] string instanceId = null)
+ {
+ var report = new MigrationReportDTO { DryRun = dryRun };
+
+ try
+ {
+ await MigrateInstancesAsync(report, dryRun, instanceId);
+ await MigrateResourcesAsync(report, dryRun, instanceId);
+ await MigrateUsersAsync(report, dryRun, instanceId);
+ await MigrateConfigurationsAsync(report, dryRun, instanceId);
+ await MigrateApplicationInstancesAsync(report, dryRun, instanceId);
+ await MigrateSectionsAsync(report, dryRun, instanceId);
+ await MigrateDevicesAsync(report, dryRun, instanceId);
+ await LinkMenuSectionsAsync(report, dryRun);
+ await EnrichPdfResourcesAsync(report, dryRun);
+ }
+ catch (Exception ex)
+ {
+ report.FatalError = ex.Message;
+ }
+
+ return new OkObjectResult(report);
+ }
+
+ // ─── Instances ────────────────────────────────────────────────────────
+
+ private async Task MigrateInstancesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var source = _instanceSvc.GetAll();
+ if (filterInstanceId != null)
+ source = source.Where(i => i.Id == filterInstanceId).ToList();
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Instances.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"Instance {old.Id} ({old.Name}) — already exists");
+ continue;
+ }
+
+ var entity = new Instance
+ {
+ Id = old.Id,
+ Name = old.Name,
+ DateCreation = old.DateCreation,
+ PinCode = old.PinCode?.ToString(),
+ };
+
+ if (!dryRun)
+ _db.Instances.Add(entity);
+
+ report.Migrated.Instances++;
+ report.Details.Add($"Instance {old.Id} ({old.Name})");
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Instance {old.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Resources ────────────────────────────────────────────────────────
+
+ private async Task MigrateResourcesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var instanceIds = filterInstanceId != null
+ ? new List { filterInstanceId }
+ : _instanceSvc.GetAll().Select(i => i.Id).ToList();
+ var source = instanceIds.SelectMany(id => _resourceSvc.GetAll(id)).ToList();
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Resources.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"Resource {old.Id} ({old.Label}) — already exists");
+ continue;
+ }
+
+ var entity = new Resource
+ {
+ Id = old.Id,
+ Type = old.Type,
+ Label = old.Label,
+ DateCreation = old.DateCreation,
+ InstanceId = old.InstanceId,
+ Url = old.Url, // NOTE: MongoDB field is "URL", mapped to Url in OldResource
+ SizeBytes = 0,
+ };
+
+ if (!dryRun)
+ _db.Resources.Add(entity);
+
+ report.Migrated.Resources++;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Resource {old.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Users ────────────────────────────────────────────────────────────
+
+ private async Task MigrateUsersAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var source = _userSvc.GetAll();
+ if (filterInstanceId != null)
+ source = source.Where(u => u.InstanceId == filterInstanceId).ToList();
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Users.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"User {old.Id} ({old.Email}) — already exists");
+ continue;
+ }
+
+ var entity = new User
+ {
+ Id = old.Id,
+ Email = old.Email,
+ Password = old.Password,
+ FirstName = old.FirstName,
+ LastName = old.LastName,
+ Token = old.Token,
+ DateCreation = old.DateCreation,
+ InstanceId = old.InstanceId,
+ Role = UserRole.ContentEditor,
+ };
+
+ if (!dryRun)
+ _db.Users.Add(entity);
+
+ report.Migrated.Users++;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"User {old.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Configurations ───────────────────────────────────────────────────
+
+ private async Task MigrateConfigurationsAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var instanceIds = filterInstanceId != null
+ ? new List { filterInstanceId }
+ : _instanceSvc.GetAll().Select(i => i.Id).ToList();
+ var source = instanceIds.SelectMany(id => _configSvc.GetAll(id)).ToList();
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Configurations.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"Configuration {old.Id} ({old.Label}) — already exists");
+ continue;
+ }
+
+ var entity = new Configuration
+ {
+ Id = old.Id,
+ InstanceId = old.InstanceId,
+ Label = old.Label,
+ Title = old.Title ?? new List(),
+ ImageId = old.ImageId,
+ ImageSource = old.ImageSource,
+ PrimaryColor = old.PrimaryColor,
+ SecondaryColor = old.SecondaryColor,
+ Languages = old.Languages ?? new List(),
+ DateCreation = old.DateCreation,
+ IsOffline = old.IsOffline,
+ LoaderImageId = old.LoaderImageId,
+ LoaderImageUrl = old.LoaderImageUrl,
+ IsQRCode = false,
+ IsSearchText = false,
+ IsSearchNumber = false,
+ };
+
+ if (!dryRun)
+ _db.Configurations.Add(entity);
+
+ report.Migrated.Configurations++;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Configuration {old.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── ApplicationInstances + AppConfigurationLinks ────────────────────
+
+ private async Task MigrateApplicationInstancesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var instanceIds = filterInstanceId != null
+ ? new List { filterInstanceId }
+ : _instanceSvc.GetAll().Select(i => i.Id).ToList();
+
+ foreach (var iid in instanceIds)
+ {
+ var configs = _configSvc.GetAll(iid);
+
+ foreach (var appType in new[] { AppType.Tablet, AppType.Mobile })
+ {
+ var relevant = appType == AppType.Tablet
+ ? configs.Where(c => c.IsTablet).ToList()
+ : configs.Where(c => c.IsMobile).ToList();
+
+ if (!relevant.Any()) continue;
+
+ var appInstanceId = $"migration-{appType.ToString().ToLower()}-{iid}";
+
+ try
+ {
+ if (!await _db.ApplicationInstances.AnyAsync(x => x.Id == appInstanceId))
+ {
+ var firstConfig = relevant.First();
+ var allLanguages = relevant
+ .Where(c => c.Languages != null)
+ .SelectMany(c => c.Languages)
+ .Distinct()
+ .ToList();
+
+ var appInstance = new ApplicationInstance
+ {
+ Id = appInstanceId,
+ InstanceId = iid,
+ AppType = appType,
+ Languages = allLanguages,
+ LoaderImageId = firstConfig.LoaderImageId,
+ LoaderImageUrl = firstConfig.LoaderImageUrl,
+ PrimaryColor = firstConfig.PrimaryColor,
+ SecondaryColor = firstConfig.SecondaryColor,
+ Configurations = new List(),
+ };
+ if (!dryRun) _db.ApplicationInstances.Add(appInstance);
+ report.Migrated.ApplicationInstances++;
+ report.Details.Add($"ApplicationInstance {appType} for instance {iid}");
+ }
+
+ foreach (var config in relevant)
+ {
+ var linkId = $"migration-{appType.ToString().ToLower()}-link-{config.Id}";
+ if (await _db.AppConfigurationLinks.AnyAsync(x => x.Id == linkId))
+ {
+ report.Skipped.Add($"AppConfigurationLink {linkId} — already exists");
+ continue;
+ }
+
+ var link = new AppConfigurationLink
+ {
+ Id = linkId,
+ ConfigurationId = config.Id,
+ ApplicationInstanceId = appInstanceId,
+ IsActive = true,
+ IsDate = appType == AppType.Tablet && config.IsDate,
+ IsHour = appType == AppType.Tablet && config.IsHour,
+ IsSectionImageBackground = appType == AppType.Tablet && config.IsSectionImageBackground,
+ RoundedValue = appType == AppType.Tablet ? config.RoundedValue : null,
+ ScreenPercentageSectionsMainPage = appType == AppType.Tablet ? config.ScreenPercentageSectionsMainPage : null,
+ LoaderImageId = config.LoaderImageId,
+ LoaderImageUrl = config.LoaderImageUrl,
+ PrimaryColor = config.PrimaryColor,
+ SecondaryColor = config.SecondaryColor,
+ };
+ if (!dryRun) _db.AppConfigurationLinks.Add(link);
+ report.Migrated.AppConfigurationLinks++;
+ }
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"ApplicationInstance {appType} {iid}: {ex.Message}");
+ }
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Sections ─────────────────────────────────────────────────────────
+
+ private async Task MigrateSectionsAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var instanceIds = filterInstanceId != null
+ ? new List { filterInstanceId }
+ : _instanceSvc.GetAll().Select(i => i.Id).ToList();
+
+ // GetAllFromConfigurationEvenSubsection inclut sections ET sous-sections
+ var configIds = instanceIds
+ .SelectMany(id => _configSvc.GetAll(id))
+ .Select(c => c.Id)
+ .ToList();
+
+ var source = configIds
+ .SelectMany(cid => _sectionSvc.GetAllFromConfigurationEvenSubsection(cid))
+ .GroupBy(s => s.Id).Select(g => g.First())
+ .ToList();
+
+ var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Sections.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"Section {old.Id} ({old.Label}, type {old.Type}) — already exists");
+ continue;
+ }
+
+ Section entity = BuildSection(old, opts, report);
+ if (entity == null) continue;
+
+ if (!dryRun)
+ _db.Sections.Add(entity);
+
+ report.Migrated.Sections++;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Section {old.Id} ({old.Label}, type {old.Type}): {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ private Section BuildSection(OldSection old, JsonSerializerOptions opts, MigrationReportDTO report)
+ {
+ // Champs communs à tous les sous-types
+ void FillBase(Section s)
+ {
+ s.Id = old.Id;
+ s.Label = old.Label ?? "";
+ s.Title = old.Title ?? new List();
+ s.Description = old.Description ?? new List();
+ s.Order = old.Order;
+ s.ConfigurationId = old.ConfigurationId;
+ s.ImageId = old.ImageId;
+ s.ImageSource = old.ImageSource;
+ s.Type = old.Type;
+ s.IsSubSection = old.IsSubSection;
+ s.ParentId = old.ParentId;
+ s.DateCreation = old.DateCreation;
+ s.InstanceId = old.InstanceId;
+ s.IsBeacon = old.IsBeacon;
+ s.BeaconId = old.BeaconId;
+ s.Latitude = old.Latitude;
+ s.Longitude = old.Longitude;
+ s.MeterZoneGPS = old.MeterZoneGPS;
+ s.IsActive = true;
+ }
+
+ switch (old.Type)
+ {
+ case SectionType.Map:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionMap
+ {
+ MapZoom = dto?.zoom ?? 18,
+ MapMapType = dto?.mapType,
+ MapTypeMapbox = dto?.mapTypeMapbox,
+ MapMapProvider = dto?.mapProvider,
+ MapResourceId = dto?.iconResourceId,
+ MapCenterLatitude = dto?.latitude,
+ MapCenterLongitude = dto?.longitude,
+ MapCategories = dto?.categories?.Select(c => new CategorieDTO
+ {
+ id = c.id,
+ label = c.label,
+ icon = c.icon,
+ order = c.order,
+ }).ToList() ?? new List(),
+ MapPoints = dto?.points?.Select(p => BuildGeoPoint(p, old.Id)).ToList()
+ ?? new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Slider:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionSlider
+ {
+ SliderContents = dto?.contents ?? new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Video:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionVideo { VideoSource = dto?.source ?? "" };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Web:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionWeb { WebSource = dto?.source ?? "" };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Menu:
+ {
+ // Les relations MenuSections sont gérées dans LinkMenuSectionsAsync
+ var s = new SectionMenu { MenuSections = new List() };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Quiz:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionQuiz
+ {
+ QuizQuestions = new List(),
+ QuizBadLevel = dto?.bad_level?.label ?? new List(),
+ QuizMediumLevel = dto?.medium_level?.label ?? new List(),
+ QuizGoodLevel = dto?.good_level?.label ?? new List(),
+ QuizGreatLevel = dto?.great_level?.label ?? new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Article:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionArticle
+ {
+ ArticleContent = dto?.content ?? new List(),
+ ArticleIsContentTop = dto?.isContentTop ?? false,
+ ArticleAudioIds = dto?.audioIds ?? new List(),
+ ArticleIsReadAudioAuto = dto?.isReadAudioAuto ?? false,
+ ArticleContents = dto?.contents ?? new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.PDF:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionPdf
+ {
+ PDFOrderedTranslationAndResources = dto?.pdfs?.Select(p =>
+ new OrderedTranslationAndResourceDTO
+ {
+ translationAndResourceDTOs = p.pdfFilesAndTitles ?? new List(),
+ order = p.order,
+ }).ToList() ?? new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Game:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionGame
+ {
+ GameMessageDebut = ToTranslationAndResourceList(dto?.messageDebut),
+ GameMessageFin = ToTranslationAndResourceList(dto?.messageFin),
+ GamePuzzleImageId = dto?.image?.resourceId,
+ GamePuzzleRows = dto?.rows ?? 3,
+ GamePuzzleCols = dto?.cols ?? 3,
+ GameType = SectionGame.GameTypes.Puzzle,
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Agenda:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionAgenda
+ {
+ IsOnlineAgenda = true,
+ AgendaResourceIds = dto?.resourceIds ?? new List(),
+ AgendaMapProvider = dto?.mapProvider,
+ EventAgendas = new List(),
+ };
+ FillBase(s);
+ return s;
+ }
+
+ case SectionType.Weather:
+ {
+ var dto = ParseData(old, opts, report);
+ var s = new SectionWeather
+ {
+ WeatherCity = dto?.city,
+ WeatherUpdatedDate = dto?.updatedDate,
+ WeatherResult = dto?.result,
+ };
+ FillBase(s);
+ return s;
+ }
+
+ default:
+ report.Errors.Add($"Section {old.Id}: unknown SectionType {old.Type} — skipped");
+ return null;
+ }
+ }
+
+ private GeoPoint BuildGeoPoint(OldGeoPointDTO p, string sectionMapId)
+ {
+ Geometry geometry = null;
+ if (p.latitude != null && p.longitude != null
+ && double.TryParse(p.latitude, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var lat)
+ && double.TryParse(p.longitude, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var lon))
+ {
+ geometry = new Point(lon, lat) { SRID = 4326 };
+ }
+
+ return new GeoPoint
+ {
+ Title = p.title ?? new List(),
+ Description = p.description ?? new List(),
+ Contents = p.contents?.Select(c => new ContentDTO
+ {
+ resourceId = c.resourceId,
+ }).ToList() ?? new List(),
+ CategorieId = p.categorieId ?? p.categorie?.id,
+ Geometry = geometry,
+ ImageResourceId = p.imageResourceId,
+ ImageUrl = p.imageUrl,
+ Schedules = p.schedules ?? new List(),
+ Prices = p.prices ?? new List(),
+ Phone = p.phone ?? new List(),
+ Email = p.email ?? new List(),
+ Site = p.site ?? new List(),
+ SectionMapId = sectionMapId,
+ };
+ }
+
+ // ─── Devices ──────────────────────────────────────────────────────────
+
+ private async Task MigrateDevicesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
+ {
+ var instanceIds = filterInstanceId != null
+ ? new List { filterInstanceId }
+ : _instanceSvc.GetAll().Select(i => i.Id).ToList();
+ var source = instanceIds.SelectMany(id => _deviceSvc.GetAll(id)).ToList();
+
+ foreach (var old in source)
+ {
+ try
+ {
+ if (await _db.Devices.AnyAsync(x => x.Id == old.Id))
+ {
+ report.Skipped.Add($"Device {old.Id} ({old.Name}) — already exists");
+ continue;
+ }
+
+ var entity = new Device
+ {
+ Id = old.Id,
+ Identifier = old.Identifier,
+ Name = old.Name,
+ IpAddressWLAN = old.IpAddressWLAN,
+ IpAddressETH = old.IpAddressETH,
+ ConfigurationId = old.ConfigurationId,
+ Connected = old.Connected,
+ DateCreation = old.DateCreation,
+ DateUpdate = old.DateUpdate < new DateTime(2000, 1, 1) ? DateTime.MinValue : old.DateUpdate,
+ BatteryLevel = old.BatteryLevel?.ToString(),
+ LastBatteryLevel = old.LastBatteryLevel,
+ ConnectionLevel = old.ConnectionLevel?.ToString(),
+ LastConnectionLevel = old.LastConnectionLevel,
+ InstanceId = old.InstanceId,
+ };
+
+ if (!dryRun)
+ _db.Devices.Add(entity);
+
+ report.Migrated.Devices++;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Device {old.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Post-processing : relier SectionMenu.MenuSections ───────────────
+
+ private async Task LinkMenuSectionsAsync(MigrationReportDTO report, bool dryRun)
+ {
+ var menus = await _db.Sections
+ .OfType()
+ .Include(m => m.MenuSections)
+ .ToListAsync();
+
+ foreach (var menu in menus)
+ {
+ if (menu.MenuSections.Count > 0)
+ {
+ report.Skipped.Add($"MenuLink {menu.Id} — already linked ({menu.MenuSections.Count} children)");
+ continue;
+ }
+
+ var oldSection = _sectionSvc.GetById(menu.Id);
+ if (oldSection?.Data == null) continue;
+
+ try
+ {
+ var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
+ var dto = JsonSerializer.Deserialize(oldSection.Data, opts);
+ if (dto?.sections == null) continue;
+
+ var childIds = dto.sections.Select(s => s.id).Where(id => id != null).ToList();
+ var children = await _db.Sections.Where(s => childIds.Contains(s.Id)).ToListAsync();
+
+ if (!dryRun)
+ {
+ menu.MenuSections = children;
+ _db.Sections.Update(menu);
+ }
+
+ report.Migrated.MenuLinks += children.Count;
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"MenuLink {menu.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Post-processing : enrichir les resource dans les JSONB PDF ─────────
+
+ private async Task EnrichPdfResourcesAsync(MigrationReportDTO report, bool dryRun)
+ {
+ var pdfSections = await _db.Sections.OfType().ToListAsync();
+
+ foreach (var pdf in pdfSections)
+ {
+ try
+ {
+ bool modified = false;
+ foreach (var ordered in pdf.PDFOrderedTranslationAndResources ?? new List())
+ {
+ foreach (var tar in ordered.translationAndResourceDTOs ?? new List())
+ {
+ if (tar.resource == null && tar.resourceId != null)
+ {
+ var resource = await _db.Resources.FindAsync(tar.resourceId);
+ if (resource != null)
+ {
+ tar.resource = resource.ToDTO();
+ modified = true;
+ }
+ }
+ }
+ }
+
+ if (modified)
+ {
+ if (!dryRun) _db.Sections.Update(pdf);
+ report.Migrated.PdfResourcesEnriched++;
+ }
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"EnrichPdf {pdf.Id}: {ex.Message}");
+ }
+ }
+
+ if (!dryRun) await _db.SaveChangesAsync();
+ }
+
+ // ─── Helpers ──────────────────────────────────────────────────────────
+
+ private T ParseData(OldSection old, JsonSerializerOptions opts, MigrationReportDTO report) where T : class
+ {
+ if (string.IsNullOrEmpty(old.Data)) return null;
+ try
+ {
+ return JsonSerializer.Deserialize(old.Data, opts);
+ }
+ catch (Exception ex)
+ {
+ report.Errors.Add($"Section {old.Id} — failed to parse Data as {typeof(T).Name}: {ex.Message}");
+ return null;
+ }
+ }
+
+ private List ToTranslationAndResourceList(List source)
+ => source ?? new List();
+ }
+
+ // ─── DTO du rapport ───────────────────────────────────────────────────────
+
+ public class MigrationReportDTO
+ {
+ public bool DryRun { get; set; }
+ public MigrationCountsDTO Migrated { get; set; } = new();
+ public List Skipped { get; set; } = new();
+ public List Errors { get; set; } = new();
+ public List Details { get; set; } = new();
+ public string FatalError { get; set; }
+ }
+
+ public class MigrationCountsDTO
+ {
+ public int Instances { get; set; }
+ public int Resources { get; set; }
+ public int Users { get; set; }
+ public int Configurations { get; set; }
+ public int ApplicationInstances { get; set; }
+ public int AppConfigurationLinks { get; set; }
+ public int Sections { get; set; }
+ public int Devices { get; set; }
+ public int MenuLinks { get; set; }
+ public int PdfResourcesEnriched { get; set; }
+ }
+}
diff --git a/ManagerService/Data/MyInfoMateDbContext.cs b/ManagerService/Data/MyInfoMateDbContext.cs
index 37cd625..cfe3323 100644
--- a/ManagerService/Data/MyInfoMateDbContext.cs
+++ b/ManagerService/Data/MyInfoMateDbContext.cs
@@ -239,6 +239,31 @@ namespace ManagerService.Data
.HasForeignKey(ma => ma.SectionEventId)
.IsRequired(false)
.OnDelete(DeleteBehavior.Cascade);
+
+ // Seed : plans d'abonnement par défaut
+ modelBuilder.Entity().HasData(
+ new SubscriptionPlan
+ {
+ Id = "plan-starter",
+ Name = "Starter",
+ StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
+ AiRequestsPerMonth = 0,
+ },
+ new SubscriptionPlan
+ {
+ Id = "plan-standard",
+ Name = "Standard",
+ StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB
+ AiRequestsPerMonth = 100,
+ },
+ new SubscriptionPlan
+ {
+ Id = "plan-premium",
+ Name = "Premium",
+ StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
+ AiRequestsPerMonth = 500,
+ }
+ );
}
}
}
diff --git a/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.Designer.cs b/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.Designer.cs
new file mode 100644
index 0000000..d2e2028
--- /dev/null
+++ b/ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.Designer.cs
@@ -0,0 +1,1505 @@
+//
+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("20260401152545_SeedSubscriptionPlans")]
+ partial class SeedSubscriptionPlans
+ {
+ ///
+ 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.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("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("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