Thomas Fransolet 7625487806 Lot B : geler le schéma, plus sécurité rapide et calculateur de stockage
LOT B — une seule migration EF (LotB_FreezeSchema) :
- SectionMap.MapResourceId → IconResourceId. L'écart (g) de la bascule tombe
  avec. Il fallait renommer aussi la propriété de navigation MapResource :
  la convention EF l'appariait au FK, la laisser aurait fabriqué un FK
  fantôme. Elle n'était utilisée nulle part ailleurs.
- SectionEvent.ParcoursIds supprimé (champ, DTO, SectionFactory, et une
  initialisation dans un montage de test).
- Instance.IsImageWatermark remplace le `instanceId == "633ee379…"` en dur
  de ResourceController.

EF a généré un RenameColumn, pas un drop+add : les icônes déjà configurées
survivent. L'avertissement de perte de données ne porte que sur le DropColumn
de ParcoursIds, ce qui est l'intention.

Non fait, et c'était une erreur de doc : « supprimer SectionEvent.IconResourceId ».
Ce champ n'existe pas — la ligne visée appartient à la classe imbriquée
MapAnnotation, partagée par SectionEvent, SectionAgenda et SectionMap, lue par
cinq contrôleurs et par GetReferencedResourceIds. La supprimer aurait cassé
les icônes d'annotation des trois types et la collecte offline.

SÉCURITÉ (lot A, même repo) :
- AuthenticationController.Authenticate : un bloc #if DEBUG écrasait l'email
  et le mot de passe reçus par un compte de test, donc toute compilation en
  Debug authentifiait n'importe quelle saisie. Retiré.
- EnableSensitiveDataLogging (qui écrit les valeurs des paramètres dans les
  logs) passe sous #if DEBUG, l'idiome déjà employé dans Startup.cs pour le
  CORS et Hangfire. Le Dockerfile publiant en -c Release, c'est un verrou réel.

LOT C1 :
- Calculateur StoragePath/SizeBytes extrait dans Helpers/ResourceStorage.cs,
  avec 13 tests fixant l'invariant des types URL. Il ferme le lien L5 : le
  backfill (C2) et l'écart (e) de la migration appelleront le même code.
- L'extraction a révélé la divergence qu'elle devait empêcher : des deux
  chemins de création de ResourceController, le chemin multipart écrivait
  SizeBytes mais laissait StoragePath nul.

dotnet build Debug et Release verts, dotnet test 143/143 (130 + 13).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:32:19 +02:00

203 lines
7.6 KiB
C#

using ManagerService.DTOs;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ManagerService.Data
{
/// <summary>
/// Instance Information
/// </summary>
public class Instance : IAuditableEntity
{
[Key]
[Required]
public string Id { get; set; }
[Required]
public string Name { get; set; } // UNIQUE !..
public DateTime DateCreation { get; set; }
public DateTime DateUpdate { get; set; }
public string PinCode { get; set; }
public bool IsPushNotification { 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; }
/// <summary>
/// Appose le filigrane du lieu sur les images téléversées. Remplace le
/// `instanceId == "633ee379…"` en dur de ResourceController.Create.
/// </summary>
public bool IsImageWatermark { get; set; }
/// <summary>Nom du guide affiché au visiteur, libre. Ex: "Léon".</summary>
public string? GuideName { get; set; }
/// <summary>
/// Personnalité du guide : instruction envoyée au modèle, jamais affichée.
/// Écrite dans une seule langue — le modèle répond dans celle du visiteur.
/// </summary>
public string? GuidePersonaPrompt { get; set; }
/// <summary>Voix Gemini TTS : "Sulafat" (Viva, féminine) ou "Umbriel" (Marco, masculine).</summary>
public string? GuideVoiceId { get; set; }
/// <summary>
/// Formulations utilisées quand le guide ne trouve pas de réponse.
/// Liste à plat : plusieurs entrées peuvent partager la même langue,
/// une est tirée au hasard parmi celles de la langue du visiteur.
/// </summary>
public List<TranslationDTO>? GuideFallbackMessages { get; set; }
public string? WebSlug { get; set; }
public string? PublicApiKey { get; set; }
public string? SubscriptionPlanId { get; set; }
[ForeignKey("SubscriptionPlanId")]
public SubscriptionPlan? SubscriptionPlan { get; set; }
public long AiTokensThisMonth { get; set; } = 0;
public string AiUsageMonthKey { get; set; } = "";
public long StorageQuotaBytes { get; set; } = 0;
public long AiTokensPerMonth { get; set; } = 0;
public bool HasStats { get; set; } = false;
public int StatsHistoryDays { get; set; } = 30;
public bool HasAdvancedStats { get; set; } = false;
public bool IsActive { get; set; } = true;
public bool IsTrialActive { get; set; } = false;
public DateTime? TrialEndsAt { get; set; }
public long TrialAiTokensUsed { get; set; } = 0;
public bool TrialCheckInEmailSent { get; set; } = false;
public bool TrialReminderEmailSent { get; set; } = false;
public bool TrialLastDayEmailSent { get; set; } = false;
public string? BillingAddress { get; set; }
public string? BillingCountry { get; set; }
public string? VatNumber { get; set; }
public decimal? VatRate { get; set; }
public string? StripeCustomerId { get; set; }
public string? StripeSubscriptionId { get; set; }
public InstanceDTO ToDTO(List<ApplicationInstanceDTO> applicationInstanceDTOs)
{
return new InstanceDTO()
{
id = Id,
name = Name,
dateCreation = DateCreation,
pinCode = PinCode,
isPushNotification = IsPushNotification,
isMobile = IsMobile,
isTablet = IsTablet,
isWeb = IsWeb,
isVR = IsVR,
isAssistant = IsAssistant,
isImageWatermark = IsImageWatermark,
guideName = GuideName,
guidePersonaPrompt = GuidePersonaPrompt,
guideVoiceId = GuideVoiceId,
guideFallbackMessages = GuideFallbackMessages,
webSlug = WebSlug,
publicApiKey = PublicApiKey,
subscriptionPlanId = SubscriptionPlanId,
subscriptionPlan = SubscriptionPlan?.ToDTO(),
aiTokensThisMonth = AiTokensThisMonth,
aiUsageMonthKey = AiUsageMonthKey,
storageQuotaBytes = StorageQuotaBytes,
aiTokensPerMonth = AiTokensPerMonth,
hasStats = HasStats,
statsHistoryDays = StatsHistoryDays,
hasAdvancedStats = HasAdvancedStats,
isTrialActive = IsTrialActive,
trialEndsAt = TrialEndsAt,
trialAiTokensUsed = TrialAiTokensUsed,
billingAddress = BillingAddress,
billingCountry = BillingCountry,
vatNumber = VatNumber,
vatRate = VatRate,
applicationInstanceDTOs = applicationInstanceDTOs
};
}
public Instance FromDTO(InstanceDTO instanceDTO)
{
Name = instanceDTO.name;
DateCreation = instanceDTO.dateCreation != null ? instanceDTO.dateCreation.Value : DateTime.Now.ToUniversalTime();
PinCode = instanceDTO.pinCode;
IsPushNotification = instanceDTO.isPushNotification ?? false;
IsMobile = instanceDTO.isMobile ?? false;
IsTablet = instanceDTO.isTablet ?? false;
IsWeb = instanceDTO.isWeb ?? false;
IsVR = instanceDTO.isVR ?? false;
IsAssistant = instanceDTO.isAssistant ?? false;
IsImageWatermark = instanceDTO.isImageWatermark ?? false;
if (instanceDTO.guideName != null)
GuideName = instanceDTO.guideName;
if (instanceDTO.guidePersonaPrompt != null)
GuidePersonaPrompt = instanceDTO.guidePersonaPrompt;
if (instanceDTO.guideVoiceId != null)
GuideVoiceId = instanceDTO.guideVoiceId;
if (instanceDTO.guideFallbackMessages != null)
GuideFallbackMessages = instanceDTO.guideFallbackMessages;
if (instanceDTO.webSlug != null)
WebSlug = instanceDTO.webSlug;
if (instanceDTO.publicApiKey != null)
PublicApiKey = instanceDTO.publicApiKey;
if (instanceDTO.subscriptionPlanId != null)
SubscriptionPlanId = instanceDTO.subscriptionPlanId;
if (instanceDTO.storageQuotaBytes.HasValue)
StorageQuotaBytes = instanceDTO.storageQuotaBytes.Value;
if (instanceDTO.aiTokensPerMonth.HasValue)
AiTokensPerMonth = instanceDTO.aiTokensPerMonth.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;
if (instanceDTO.billingAddress != null)
BillingAddress = instanceDTO.billingAddress;
if (instanceDTO.billingCountry != null)
BillingCountry = instanceDTO.billingCountry;
if (instanceDTO.vatNumber != null)
VatNumber = instanceDTO.vatNumber;
if (instanceDTO.vatRate.HasValue)
VatRate = instanceDTO.vatRate.Value;
return this;
}
}
}