Thomas Fransolet af6b5b76ef Self-service onboarding (Stripe + Resend) + Postgres v3 schema (pgvector, media, visitor questions)
75 fichiers, ~1 mois de travail depuis f222861 (17/07). Contenu :

Onboarding self-service — jamais exécuté de bout en bout
  OnboardingController, StripeWebhookController, StripeService, ResendEmailService
  + IEmailService (10 templates), EmailTemplates/, TrialLifecycleService (Hangfire),
  PasswordTokenHelper, SlugHelper, 5 DTOs. Essai 14 j, Stripe customer/Checkout/Tax,
  mot de passe oublié + invitation user, plafond IA d'essai.

Schéma Postgres v3 — passe pendant que la base est vide
  ContentEmbedding + index HNSW (vector_cosine_ops), IEmbeddingService +
  GoogleEmbeddingService (gemini-embedding-001, 768 dims), Deployment/Dockerfile.postgres
  (postgis 3.4.3 + pgvector 0.8.6, épinglé par digest — un tag mobile rejouerait le
  warning de collation glibc). Colonnes Resource : StoragePath, FileName,
  IncludeInAiKnowledge, AiIndexStatus + nouveaux ResourceType ajoutés EN FIN d'enum.

Guide IA
  Champs Guide* sur Instance + InstanceDTO, AssistantService lit la configuration client
  dans les 4 blocs de prompt (ton codé en dur retiré, règle hors-sujet ajoutée aux deux
  variantes qui n'en avaient pas), IHttpClientFactory à la place des new HttpClient().
  Table VisitorQuestion + ConversationId sur AiChatRequest.

Stats
  Rétention unifiée à 13 mois (instances ET plans), VisitEventPurgeService.

Nettoyage
  IsStepLocked / IsHiddenInitially / FactContent supprimés de GuidedStep — IsStepLocked
  rendait une étape définitivement infranchissable même après réussite.
  SectionMap allégé (-57 lignes).

Tests
  SectionParcoursControllerTests, FakeConfiguration, FakeEmailService.
  ContentEmbedding a cassé 116 tests sur 124 (EF InMemory ne connaît pas Vector) :
  l'entité est exclue quand le provider n'est pas Npgsql. Conséquence assumée —
  le vector store n'est couvert par aucun test. dotnet test 124/124.

10 migrations EF. Base locale à jour, dotnet build 0 erreur.
Rien n'est en prod : la bascule Mongo → Postgres est décrite dans DOCS/STATUS.md §1quinquies.

⚠️ appsettings.json contient les clés Stripe (test) et Resend (prod) en clair — à rotationner.
2026-08-09 22:11:48 +02:00

195 lines
7.2 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>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,
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;
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;
}
}
}