Sauvegarde des champs du guide et unicité des canaux par instance

Updateinstance recopiait les champs un par un et n'avait jamais été étendu
aux champs du guide : GuideName, GuidePersonaPrompt, GuideVoiceId,
IsVisitorQuestionCollectionEnabled et GuideFallbackMessages étaient reçus
puis ignorés, donc l'écran Guide IA ne sauvegardait rien. Reprend les mêmes
gardes que Instance.FromDTO.

Rien n'empêchait deux ApplicationInstance du même AppType sur une instance,
alors que manager-app et AiController résolvent tous deux le canal par un
FirstOrDefault sur (InstanceId, AppType) : un doublon rendait le canal
ambigu, l'assistant pouvant s'afficher activé pendant que l'API répondait
Forbid. Ajoute l'index unique et le contrôle en amont dans Create (409).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-14 15:30:47 +02:00
parent cb76df5ddf
commit ea237d2913
6 changed files with 1970 additions and 1 deletions

View File

@ -79,7 +79,16 @@ namespace ManagerService.Controllers
if (newApplicationInstanceDTO == null)
throw new ArgumentNullException("Application instance param is null");
// Todo add some verification ?
// Tout le code (manager-app, AiController) résout une application par
// `(InstanceId, AppType)` via un FirstOrDefault : un doublon rend le canal
// ambigu — l'assistant s'affiche activé alors que l'API répond Forbid.
bool alreadyExists = _myInfoMateDbContext.ApplicationInstances.Any(
ai => ai.InstanceId == newApplicationInstanceDTO.instanceId
&& ai.AppType == newApplicationInstanceDTO.appType);
if (alreadyExists)
throw new InvalidOperationException($"An application instance of type {newApplicationInstanceDTO.appType} already exists for this instance");
ApplicationInstance applicationInstance = new ApplicationInstance().FromDTO(newApplicationInstanceDTO);
applicationInstance.Id = idService.GenerateHexId();

View File

@ -211,6 +211,18 @@ namespace ManagerService.Controllers
instance.IsWeb = updatedInstance.isWeb ?? instance.IsWeb;
instance.IsVR = updatedInstance.isVR ?? instance.IsVR;
instance.IsAssistant = updatedInstance.isAssistant ?? instance.IsAssistant;
if (updatedInstance.guideName != null)
instance.GuideName = updatedInstance.guideName;
if (updatedInstance.guidePersonaPrompt != null)
instance.GuidePersonaPrompt = updatedInstance.guidePersonaPrompt;
if (updatedInstance.guideVoiceId != null)
instance.GuideVoiceId = updatedInstance.guideVoiceId;
if (updatedInstance.isVisitorQuestionCollectionEnabled != null)
instance.IsVisitorQuestionCollectionEnabled = updatedInstance.isVisitorQuestionCollectionEnabled.Value;
if (updatedInstance.guideFallbackMessages != null)
instance.GuideFallbackMessages = updatedInstance.guideFallbackMessages;
var previousPlanId = instance.SubscriptionPlanId;
var previousAiTokens = instance.AiTokensPerMonth;

View File

@ -16,6 +16,7 @@ namespace ManagerService.Data
/// allowing apps to use one or multiple configurations.
/// </summary>
[Index(nameof(InstanceId))]
[Index(nameof(InstanceId), nameof(AppType), IsUnique = true)]
public class ApplicationInstance : IAuditableEntity
{
[Key]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ManagerService.Migrations
{
/// <inheritdoc />
public partial class UniqueApplicationInstancePerAppType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_ApplicationInstances_InstanceId_AppType",
table: "ApplicationInstances",
columns: new[] { "InstanceId", "AppType" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_ApplicationInstances_InstanceId_AppType",
table: "ApplicationInstances");
}
}
}

View File

@ -193,6 +193,9 @@ namespace ManagerService.Migrations
b.HasIndex("SectionEventId");
b.HasIndex("InstanceId", "AppType")
.IsUnique();
b.ToTable("ApplicationInstances");
});