Aligne les plans d'abonnement sur la grille de tarifs
La base semait Starter / Standard / Premium / Essentiel : les noms ET les
quotas avaient divergé de la grille de myinfomate-landing, sans que rien ne
le signale puisqu'aucun code n'indexe sur ces noms.
Écarts corrigés, pas seulement des renommages :
- Essentiel portait 500 000 jetons IA alors que la grille dit « Assistant IA
visiteur : non inclus ». Passé à 0.
- Standard → Pro : 10 GB au lieu des 15 GB vendus, et 5 M jetons alors que
l'IA n'est pas incluse non plus. Corrigé aux deux endroits.
- Essentiel et Pro avaient 395 jours de rétention de stats alors que la
grille annonce des stats basiques (30 jours).
- Premium était déjà conforme.
- plan-enterprise créé : stockage illimité, IA en quota sur mesure.
- plan-starter supprimé — aucun équivalent commercial, et son 0 jeton en
faisait un piège pour qui cherchait « le plan d'entrée ».
L'assistant IA n'étant inclus qu'à partir de Premium, il se vend en add-on
sur les plans inférieurs : surcharger Instance.AiTokensPerMonth suffit,
ApplyPlanQuotas ne repartant du plan qu'au changement de plan.
⚠️ La migration a été réécrite : EF avait scaffoldé les DeleteData en tête,
ce qui aurait buté sur la clé étrangère Instances.SubscriptionPlanId — ou
laissé des instances sans plan, donc à quotas nuls, donc sans IA ni
stockage. Les nouveaux plans sont insérés d'abord, les instances repointées
ensuite, les suppressions en dernier.
Sémantique du 0 laissée intacte et documentée : illimité pour le stockage,
pas d'IA pour les jetons. D'où long.MaxValue sur Enterprise.
dotnet build vert, dotnet test 143/143, migration appliquée en local.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7625487806
commit
a80096d72b
@ -529,47 +529,61 @@ namespace ManagerService.Data
|
||||
// profondeur (HasAdvancedStats), pas sur la durée — décidé le 2026-08-09.
|
||||
// Au-delà, les VisitEvent sont purgés (job `visit-events-purge`).
|
||||
|
||||
// Seed : plans d'abonnement par défaut
|
||||
// Seed : plans d'abonnement, alignés sur la grille de tarifs de
|
||||
// myinfomate-landing (source de vérité) — Essentiel 39€ / Pro 99€ /
|
||||
// Premium 179€ / Enterprise sur devis.
|
||||
//
|
||||
// Ce que le plan NE porte pas : app native, offline + beacons, push et
|
||||
// traduction automatique sont dans la grille mais pas dans cette table.
|
||||
// Ils se règlent par instance (IsMobile, IsWeb…) : la base ne les applique
|
||||
// pas, l'affectation le fait.
|
||||
//
|
||||
// ⚠️ Sémantique de 0, asymétrique et volontaire : pour StorageQuotaBytes,
|
||||
// 0 = illimité ; pour AiTokensPerMonth, 0 = pas d'IA. D'où la sentinelle
|
||||
// long.MaxValue sur Enterprise plutôt qu'un 0 qui le priverait d'assistant.
|
||||
modelBuilder.Entity<SubscriptionPlan>().HasData(
|
||||
new SubscriptionPlan
|
||||
{
|
||||
Id = "plan-starter",
|
||||
Name = "Starter",
|
||||
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
|
||||
Id = "plan-essentiel",
|
||||
Name = "Essentiel",
|
||||
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
|
||||
// L'assistant IA n'est pas inclus dans ce plan. Il se vend en add-on,
|
||||
// activé en surchargeant Instance.AiTokensPerMonth sans changer de plan
|
||||
// (ApplyPlanQuotas ne repart du plan qu'au changement de plan).
|
||||
AiTokensPerMonth = 0,
|
||||
HasStats = false,
|
||||
StatsHistoryDays = 30,
|
||||
HasStats = true,
|
||||
StatsHistoryDays = 30, // « Visiteurs / jour »
|
||||
HasAdvancedStats = false,
|
||||
},
|
||||
new SubscriptionPlan
|
||||
{
|
||||
Id = "plan-standard",
|
||||
Name = "Standard",
|
||||
StorageQuotaBytes = 10L * 1024 * 1024 * 1024, // 10 GB
|
||||
AiTokensPerMonth = 5_000_000, // ~500 req/mois * ~10k tokens/req estimés, à ajuster selon l'usage réel observé
|
||||
Id = "plan-pro",
|
||||
Name = "Pro",
|
||||
StorageQuotaBytes = 15L * 1024 * 1024 * 1024, // 15 GB
|
||||
AiTokensPerMonth = 0, // non inclus, cf. Essentiel
|
||||
HasStats = true,
|
||||
StatsHistoryDays = StatsRetentionDays,
|
||||
StatsHistoryDays = 30,
|
||||
HasAdvancedStats = false,
|
||||
},
|
||||
new SubscriptionPlan
|
||||
{
|
||||
Id = "plan-premium",
|
||||
Name = "Premium",
|
||||
StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
|
||||
AiTokensPerMonth = 20_000_000, // ~2000 req/mois * ~10k tokens/req estimés, à ajuster selon l'usage réel observé
|
||||
StorageQuotaBytes = 50L * 1024 * 1024 * 1024, // 50 GB
|
||||
AiTokensPerMonth = 20_000_000, // ~2 000 req/mois * ~10k jetons
|
||||
HasStats = true,
|
||||
StatsHistoryDays = StatsRetentionDays,
|
||||
HasAdvancedStats = true,
|
||||
},
|
||||
new SubscriptionPlan
|
||||
{
|
||||
Id = "plan-essentiel",
|
||||
Name = "Essentiel",
|
||||
StorageQuotaBytes = 1L * 1024 * 1024 * 1024, // 1 GB
|
||||
AiTokensPerMonth = 500_000, // ~50 req/mois * ~10k tokens/req estimés — IA reste surtout un add-on payant sur ce plan
|
||||
Id = "plan-enterprise",
|
||||
Name = "Enterprise",
|
||||
StorageQuotaBytes = 0, // 0 = illimité
|
||||
AiTokensPerMonth = long.MaxValue, // « quota sur mesure »
|
||||
HasStats = true,
|
||||
StatsHistoryDays = StatsRetentionDays,
|
||||
HasAdvancedStats = false,
|
||||
HasAdvancedStats = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
1883
ManagerService/Migrations/20260811140135_AlignSubscriptionPlansWithPricing.Designer.cs
generated
Normal file
1883
ManagerService/Migrations/20260811140135_AlignSubscriptionPlansWithPricing.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,92 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace ManagerService.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AlignSubscriptionPlansWithPricing : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Ordre imposé : les nouveaux plans d'abord, le repointage ensuite, les
|
||||
// suppressions en dernier. EF avait scaffoldé les DeleteData en tête, ce qui
|
||||
// aurait buté sur la clé étrangère Instances.SubscriptionPlanId — ou laissé
|
||||
// des instances sans plan, donc à quotas nuls, donc sans IA ni stockage.
|
||||
migrationBuilder.InsertData(
|
||||
table: "SubscriptionPlans",
|
||||
columns: new[] { "Id", "AiTokensPerMonth", "HasAdvancedStats", "HasStats", "Name", "StatsHistoryDays", "StorageQuotaBytes" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ "plan-enterprise", 9223372036854775807L, true, true, "Enterprise", 395, 0L },
|
||||
{ "plan-pro", 0L, false, true, "Pro", 30, 16106127360L }
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-essentiel",
|
||||
columns: new[] { "AiTokensPerMonth", "StatsHistoryDays" },
|
||||
values: new object[] { 0L, 30 });
|
||||
|
||||
// « Standard » devient « Pro » : même offre, nom aligné sur la grille.
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE ""Instances"" SET ""SubscriptionPlanId"" = 'plan-pro' WHERE ""SubscriptionPlanId"" = 'plan-standard';");
|
||||
|
||||
// « Starter » n'a aucun équivalent commercial et donnait 0 jeton : les
|
||||
// instances qui y pointaient sont détachées plutôt que rangées dans une
|
||||
// offre qu'elles n'ont pas achetée. Un plan nul est visible à l'écran,
|
||||
// un mauvais plan ne l'est pas.
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE ""Instances"" SET ""SubscriptionPlanId"" = NULL WHERE ""SubscriptionPlanId"" = 'plan-starter';");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-standard");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-starter");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-enterprise");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-pro");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "SubscriptionPlans",
|
||||
keyColumn: "Id",
|
||||
keyValue: "plan-essentiel",
|
||||
columns: new[] { "AiTokensPerMonth", "StatsHistoryDays" },
|
||||
values: new object[] { 500000L, 395 });
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "SubscriptionPlans",
|
||||
columns: new[] { "Id", "AiTokensPerMonth", "HasAdvancedStats", "HasStats", "Name", "StatsHistoryDays", "StorageQuotaBytes" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ "plan-standard", 5000000L, false, true, "Standard", 395, 10737418240L },
|
||||
{ "plan-starter", 0L, false, false, "Starter", 30, 1073741824L }
|
||||
});
|
||||
|
||||
// Repointage inverse. Les instances détachées de plan-starter ne sont pas
|
||||
// rattachées : rien ne dit lesquelles l'étaient.
|
||||
migrationBuilder.Sql(
|
||||
@"UPDATE ""Instances"" SET ""SubscriptionPlanId"" = 'plan-standard' WHERE ""SubscriptionPlanId"" = 'plan-pro';");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1153,23 +1153,23 @@ namespace ManagerService.Migrations
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = "plan-starter",
|
||||
Id = "plan-essentiel",
|
||||
AiTokensPerMonth = 0L,
|
||||
HasAdvancedStats = false,
|
||||
HasStats = false,
|
||||
Name = "Starter",
|
||||
HasStats = true,
|
||||
Name = "Essentiel",
|
||||
StatsHistoryDays = 30,
|
||||
StorageQuotaBytes = 1073741824L
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "plan-standard",
|
||||
AiTokensPerMonth = 5000000L,
|
||||
Id = "plan-pro",
|
||||
AiTokensPerMonth = 0L,
|
||||
HasAdvancedStats = false,
|
||||
HasStats = true,
|
||||
Name = "Standard",
|
||||
StatsHistoryDays = 395,
|
||||
StorageQuotaBytes = 10737418240L
|
||||
Name = "Pro",
|
||||
StatsHistoryDays = 30,
|
||||
StorageQuotaBytes = 16106127360L
|
||||
},
|
||||
new
|
||||
{
|
||||
@ -1183,13 +1183,13 @@ namespace ManagerService.Migrations
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = "plan-essentiel",
|
||||
AiTokensPerMonth = 500000L,
|
||||
HasAdvancedStats = false,
|
||||
Id = "plan-enterprise",
|
||||
AiTokensPerMonth = 9223372036854775807L,
|
||||
HasAdvancedStats = true,
|
||||
HasStats = true,
|
||||
Name = "Essentiel",
|
||||
Name = "Enterprise",
|
||||
StatsHistoryDays = 395,
|
||||
StorageQuotaBytes = 1073741824L
|
||||
StorageQuotaBytes = 0L
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user