Add seed subscriptionplan + wip migration controller
This commit is contained in:
parent
f72d94f30f
commit
eff4f7ba5c
803
ManagerService/Controllers/MigrationController.cs
Normal file
803
ManagerService/Controllers/MigrationController.cs
Normal file
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[HttpPost("run")]
|
||||
[ProducesResponseType(typeof(MigrationReportDTO), 200)]
|
||||
[ProducesResponseType(typeof(string), 500)]
|
||||
public async Task<ObjectResult> 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<string> { 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<string> { 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<TranslationDTO>(),
|
||||
ImageId = old.ImageId,
|
||||
ImageSource = old.ImageSource,
|
||||
PrimaryColor = old.PrimaryColor,
|
||||
SecondaryColor = old.SecondaryColor,
|
||||
Languages = old.Languages ?? new List<string>(),
|
||||
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<string> { 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<AppConfigurationLink>(),
|
||||
};
|
||||
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<string> { 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<TranslationDTO>();
|
||||
s.Description = old.Description ?? new List<TranslationDTO>();
|
||||
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<OldMapDTO>(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<CategorieDTO>(),
|
||||
MapPoints = dto?.points?.Select(p => BuildGeoPoint(p, old.Id)).ToList()
|
||||
?? new List<GeoPoint>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Slider:
|
||||
{
|
||||
var dto = ParseData<OldSliderDTO>(old, opts, report);
|
||||
var s = new SectionSlider
|
||||
{
|
||||
SliderContents = dto?.contents ?? new List<ContentDTO>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Video:
|
||||
{
|
||||
var dto = ParseData<OldVideoDTO>(old, opts, report);
|
||||
var s = new SectionVideo { VideoSource = dto?.source ?? "" };
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Web:
|
||||
{
|
||||
var dto = ParseData<OldWebDTO>(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<Section>() };
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Quiz:
|
||||
{
|
||||
var dto = ParseData<OldQuizzDTO>(old, opts, report);
|
||||
var s = new SectionQuiz
|
||||
{
|
||||
QuizQuestions = new List<QuizQuestion>(),
|
||||
QuizBadLevel = dto?.bad_level?.label ?? new List<TranslationAndResourceDTO>(),
|
||||
QuizMediumLevel = dto?.medium_level?.label ?? new List<TranslationAndResourceDTO>(),
|
||||
QuizGoodLevel = dto?.good_level?.label ?? new List<TranslationAndResourceDTO>(),
|
||||
QuizGreatLevel = dto?.great_level?.label ?? new List<TranslationAndResourceDTO>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Article:
|
||||
{
|
||||
var dto = ParseData<OldArticleDTO>(old, opts, report);
|
||||
var s = new SectionArticle
|
||||
{
|
||||
ArticleContent = dto?.content ?? new List<TranslationDTO>(),
|
||||
ArticleIsContentTop = dto?.isContentTop ?? false,
|
||||
ArticleAudioIds = dto?.audioIds ?? new List<TranslationDTO>(),
|
||||
ArticleIsReadAudioAuto = dto?.isReadAudioAuto ?? false,
|
||||
ArticleContents = dto?.contents ?? new List<ContentDTO>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.PDF:
|
||||
{
|
||||
var dto = ParseData<OldPdfDTO>(old, opts, report);
|
||||
var s = new SectionPdf
|
||||
{
|
||||
PDFOrderedTranslationAndResources = dto?.pdfs?.Select(p =>
|
||||
new OrderedTranslationAndResourceDTO
|
||||
{
|
||||
translationAndResourceDTOs = p.pdfFilesAndTitles ?? new List<TranslationAndResourceDTO>(),
|
||||
order = p.order,
|
||||
}).ToList() ?? new List<OrderedTranslationAndResourceDTO>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Game:
|
||||
{
|
||||
var dto = ParseData<OldPuzzleDTO>(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<OldAgendaDTO>(old, opts, report);
|
||||
var s = new SectionAgenda
|
||||
{
|
||||
IsOnlineAgenda = true,
|
||||
AgendaResourceIds = dto?.resourceIds ?? new List<TranslationDTO>(),
|
||||
AgendaMapProvider = dto?.mapProvider,
|
||||
EventAgendas = new List<EventAgenda>(),
|
||||
};
|
||||
FillBase(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
case SectionType.Weather:
|
||||
{
|
||||
var dto = ParseData<OldWeatherDTO>(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<TranslationDTO>(),
|
||||
Description = p.description ?? new List<TranslationDTO>(),
|
||||
Contents = p.contents?.Select(c => new ContentDTO
|
||||
{
|
||||
resourceId = c.resourceId,
|
||||
}).ToList() ?? new List<ContentDTO>(),
|
||||
CategorieId = p.categorieId ?? p.categorie?.id,
|
||||
Geometry = geometry,
|
||||
ImageResourceId = p.imageResourceId,
|
||||
ImageUrl = p.imageUrl,
|
||||
Schedules = p.schedules ?? new List<TranslationDTO>(),
|
||||
Prices = p.prices ?? new List<TranslationDTO>(),
|
||||
Phone = p.phone ?? new List<TranslationDTO>(),
|
||||
Email = p.email ?? new List<TranslationDTO>(),
|
||||
Site = p.site ?? new List<TranslationDTO>(),
|
||||
SectionMapId = sectionMapId,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Devices ──────────────────────────────────────────────────────────
|
||||
|
||||
private async Task MigrateDevicesAsync(MigrationReportDTO report, bool dryRun, string filterInstanceId)
|
||||
{
|
||||
var instanceIds = filterInstanceId != null
|
||||
? new List<string> { 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<SectionMenu>()
|
||||
.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<OldMenuDTO>(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<SectionPdf>().ToListAsync();
|
||||
|
||||
foreach (var pdf in pdfSections)
|
||||
{
|
||||
try
|
||||
{
|
||||
bool modified = false;
|
||||
foreach (var ordered in pdf.PDFOrderedTranslationAndResources ?? new List<OrderedTranslationAndResourceDTO>())
|
||||
{
|
||||
foreach (var tar in ordered.translationAndResourceDTOs ?? new List<TranslationAndResourceDTO>())
|
||||
{
|
||||
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<T>(OldSection old, JsonSerializerOptions opts, MigrationReportDTO report) where T : class
|
||||
{
|
||||
if (string.IsNullOrEmpty(old.Data)) return null;
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<T>(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<TranslationAndResourceDTO> ToTranslationAndResourceList(List<TranslationAndResourceDTO> source)
|
||||
=> source ?? new List<TranslationAndResourceDTO>();
|
||||
}
|
||||
|
||||
// ─── DTO du rapport ───────────────────────────────────────────────────────
|
||||
|
||||
public class MigrationReportDTO
|
||||
{
|
||||
public bool DryRun { get; set; }
|
||||
public MigrationCountsDTO Migrated { get; set; } = new();
|
||||
public List<string> Skipped { get; set; } = new();
|
||||
public List<string> Errors { get; set; } = new();
|
||||
public List<string> 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; }
|
||||
}
|
||||
}
|
||||
@ -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<SubscriptionPlan>().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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1505
ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.Designer.cs
generated
Normal file
1505
ManagerService/Migrations/20260401152545_SeedSubscriptionPlans.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class SeedSubscriptionPlans : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.InsertData(
|
||||
table: "SubscriptionPlans",
|
||||
columns: new[] { "Id", "AiRequestsPerMonth", "Name", "StorageQuotaBytes" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ "plan-premium", 500, "Premium", 53687091200L },
|
||||
{ "plan-standard", 100, "Standard", 10737418240L },
|
||||
{ "plan-starter", 0, "Starter", 1073741824L }
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-premium");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-standard");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-starter");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -889,6 +889,29 @@ namespace ManagerService.Migrations
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("SubscriptionPlans");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = "plan-starter",
|
||||
AiRequestsPerMonth = 0,
|
||||
Name = "Starter",
|
||||
StorageQuotaBytes = 1073741824L
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "plan-standard",
|
||||
AiRequestsPerMonth = 100,
|
||||
Name = "Standard",
|
||||
StorageQuotaBytes = 10737418240L
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "plan-premium",
|
||||
AiRequestsPerMonth = 500,
|
||||
Name = "Premium",
|
||||
StorageQuotaBytes = 53687091200L
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ManagerService.Data.User", b =>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user