Rename Entitlement -> Feature (#7016)
What changed, and why it matters
This commit is a straightforward internal rename from the word 'Entitlement' to the word 'Feature' across the new subscriptions/monetization plugin code. It changes class names, database table names, property names, method names, UI labels, and test helpers, but does not alter any security logic, access checks, or data handling behavior. There is no indication this fixes or introduces a security issue.
No security action required. Treat as a normal code-quality rename. Reviewers should verify that the migration rename is compatible with any existing pre-release deployments that already used the old table names, since the migration file itself was edited rather than adding a new migration.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is a broad refactoring of the BTCPay Server subscriptions and monetization modules. It renames entities (EntitlementData -> FeatureData, PlanEntitlementData -> PlanFeatureData), DbSets, migration table names (subs_entitlements -> subs_features, subs_plans_entitlements -> subs_plans_features), helper methods (HasEntitlements -> HasFeature, FetchPlanEntitlementsAsync -> FetchPlanFeaturesAsync), view model properties, Razor view IDs, and localized strings. The constant MonetizationEntitlements.CanAccess becomes MonetizationFeatures.CanAccess with the same value ‘can-access’. Logic such as login lockout checks, plan creation, plan editing, and webhook payloads remains functionally identical. A tiny unrelated test change adds a Playwright DOMContentLoaded wait in UnitTest1.cs.
Changed components
BTCPayServer.Data subscriptions data models and migrationsBTCPayServer.Plugins.Subscriptions controllers and viewsBTCPayServer.Plugins.Monetization controllers and servicesBTCPayServer.Client subscription plan API modelBTCPayServer.Tests subscription and monetization testsInspect captured patch +305 / −304
diff --git a/BTCPayServer.Client/Models/SubscriptionPlanModel.cs b/BTCPayServer.Client/Models/SubscriptionPlanModel.cs
index 3877189..4108819 100644
--- a/BTCPayServer.Client/Models/SubscriptionPlanModel.cs
+++ b/BTCPayServer.Client/Models/SubscriptionPlanModel.cs
@@ -34,5 +34,5 @@ public class SubscriptionPlanModel
public string Description { get; set; }
public int MemberCount { get; set; }
public bool OptimisticActivation { get; set; }
- public string[] Entitlements { get; set; }
+ public string[] Features { get; set; }
}
diff --git a/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContext.Subscriptions.cs b/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContext.Subscriptions.cs
index b86c87f..16a2787 100644
--- a/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContext.Subscriptions.cs
+++ b/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContext.Subscriptions.cs
@@ -5,8 +5,8 @@ namespace BTCPayServer.Data;
public partial class ApplicationDbContext
{
- public DbSet<EntitlementData> Entitlements { get; set; }
- public DbSet<PlanEntitlementData> PlanEntitlements { get; set; }
+ public DbSet<FeatureData> Features { get; set; }
+ public DbSet<PlanFeatureData> PlanFeatures { get; set; }
public DbSet<OfferingData> Offerings { get; set; }
public DbSet<SubscriberData> Subscribers { get; set; }
public DbSet<SubscriberCredit> Credits { get; set; }
@@ -25,8 +25,8 @@ public partial class ApplicationDbContext
PlanChangeData.OnModelCreating(builder, Database);
PortalSessionData.OnModelCreating(builder, Database);
PlanCheckoutData.OnModelCreating(builder, Database);
- EntitlementData.OnModelCreating(builder, Database);
- PlanEntitlementData.OnModelCreating(builder, Database);
+ FeatureData.OnModelCreating(builder, Database);
+ PlanFeatureData.OnModelCreating(builder, Database);
OfferingData.OnModelCreating(builder, Database);
SubscriberData.OnModelCreating(builder, Database);
SubscriberInvoiceData.OnModelCreating(builder, Database);
diff --git a/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContextExtensions.Subscriptions.cs b/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContextExtensions.Subscriptions.cs
index 11683f8..75d4ca1 100644
--- a/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContextExtensions.Subscriptions.cs
+++ b/BTCPayServer.Data/Data/Subscriptions/ApplicationDbContextExtensions.Subscriptions.cs
@@ -25,21 +25,21 @@ public static partial class ApplicationDbContextExtensions
if (storeId is not null && plan?.Offering.App.StoreDataId != storeId)
return null;
if (plan is not null)
- await plan.EnsureEntitlementLoaded(plans);
+ await plan.EnsureFeatureLoaded(plans);
return plan;
}
- public static async Task<bool> HasEntitlements(this DbSet<PlanData> plans, string planId, string entitlementCustomId)
+ public static async Task<bool> HasFeature(this DbSet<PlanData> plans, string planId, string featureCustomId)
{
var connection = plans.GetDbConnection();
return await connection.ExecuteScalarAsync<bool>("""
- SELECT true FROM subs_plans_entitlements pe
- JOIN subs_entitlements e ON e.id = pe.entitlement_id
- WHERE pe.plan_id = @planId AND e.custom_id = @entitlementCustomId
- """, new{ planId, entitlementCustomId });
+ SELECT true FROM subs_plans_features pe
+ JOIN subs_features e ON e.id = pe.feature_id
+ WHERE pe.plan_id = @planId AND e.custom_id = @featureCustomId
+ """, new{ planId, featureCustomId });
}
- public static async Task FetchPlanEntitlementsAsync<T>(this DbSet<T> ctx, IEnumerable<PlanData> plans) where T : class
+ public static async Task FetchPlanFeaturesAsync<T>(this DbSet<T> ctx, IEnumerable<PlanData> plans) where T : class
{
var planIds = plans.Select(p => p.Id).Distinct().ToArray();
var result = await ctx.GetDbConnection()
@@ -52,48 +52,48 @@ public static partial class ApplicationDbContextExtensions
(
"""
SELECT pId,
- array_agg(spe.entitlement_id),
+ array_agg(spe.feature_id),
array_agg(se.custom_id),
array_agg(se.description)
FROM unnest(@planIds) pId
- JOIN subs_plans_entitlements spe ON spe.plan_id = pId
- JOIN subs_entitlements se ON se.id = spe.entitlement_id
+ JOIN subs_plans_features spe ON spe.plan_id = pId
+ JOIN subs_features se ON se.id = spe.feature_id
GROUP BY 1
""", new { planIds }
);
var res = result.ToDictionary(x => x.Id, x => x);
foreach (var plan in plans)
{
- plan.PlanEntitlements = new();
+ plan.PlanFeatures = new();
if (res.TryGetValue(plan.Id, out var r))
{
for (int i = 0; i < r.ECIds.Length; i++)
{
- var pe = new PlanEntitlementData();
+ var pe = new PlanFeatureData();
pe.Plan = plan;
pe.PlanId = plan.Id;
- pe.EntitlementId = r.EIds[i];
- pe.Entitlement = new()
+ pe.FeatureId = r.EIds[i];
+ pe.Feature = new()
{
Id = r.EIds[i],
CustomId = r.ECIds[i],
Description = r.EDesc[i],
};
- plan.PlanEntitlements.Add(pe);
+ plan.PlanFeatures.Add(pe);
}
}
}
}
- public static Task FetchPlanEntitlementsAsync<T>(this DbSet<T> ctx, PlanData plan) where T : class
- => FetchPlanEntitlementsAsync(ctx, new[] { plan });
+ public static Task FetchPlanFeaturesAsync<T>(this DbSet<T> ctx, PlanData plan) where T : class
+ => FetchPlanFeaturesAsync(ctx, new[] { plan });
public static async Task<OfferingData?> GetOfferingData(this DbSet<OfferingData> offerings, string offeringId, string? storeId = null)
{
var offering = offerings
- .Include(o => o.Entitlements)
+ .Include(o => o.Features)
.Include(o => o.Plans)
.Include(o => o.App)
.ThenInclude(o => o.StoreData)
@@ -118,7 +118,7 @@ public static partial class ApplicationDbContextExtensions
.Where(c => c.Id == checkoutId)
.FirstOrDefaultAsync();
if (checkout is not null)
- await FetchPlanEntitlementsAsync(checkouts, checkout.Plan);
+ await FetchPlanFeaturesAsync(checkouts, checkout.Plan);
return checkout;
}
@@ -162,7 +162,7 @@ public static partial class ApplicationDbContextExtensions
(storeId != null && result.Plan?.Offering?.App?.StoreDataId != storeId) ||
(planId != null && result.PlanId != planId))
return null;
- await FetchPlanEntitlementsAsync(dbSet, result.Plan);
+ await FetchPlanFeaturesAsync(dbSet, result.Plan);
return result;
}
@@ -177,7 +177,7 @@ public static partial class ApplicationDbContextExtensions
{
var sub = await subscribers.IncludeAll().Where(s => s.Id == id).FirstOrDefaultAsync();
if (sub != null)
- await FetchPlanEntitlementsAsync(subscribers, sub.Plan);
+ await FetchPlanFeaturesAsync(subscribers, sub.Plan);
return sub;
}
diff --git a/BTCPayServer.Data/Data/Subscriptions/EntitlementData.cs b/BTCPayServer.Data/Data/Subscriptions/EntitlementData.cs
deleted file mode 100644
index 94e9239..0000000
--- a/BTCPayServer.Data/Data/Subscriptions/EntitlementData.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-#nullable enable
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-
-namespace BTCPayServer.Data.Subscriptions;
-
-[Table("subs_entitlements")]
-public class EntitlementData
-{
- /// <summary>
- /// The internal ID of the entitlement, we only really use it in
- /// SQL queries. This should not be exposed.
- /// </summary>
- [Required]
- [Column("id")]
- [Key]
- public long Id { get; set; }
-
- /// <summary>
- /// The ID selected by the user, scoped at the offering level.
- /// </summary>
- [Required]
- [Column("custom_id")]
- public string CustomId { get; set; } = null!;
- [Required]
- [Column("offering_id")]
- public string OfferingId { get; set; } = null!;
-
- [ForeignKey(nameof(OfferingId))]
- public OfferingData Offering { get; set; } = null!;
-
- [Column("description")]
- public string? Description { get; set; }
-
- public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
- {
- var b = builder.Entity<EntitlementData>();
- b.HasKey(x => x.Id);
- b.Property(x => x.Id).UseIdentityAlwaysColumn();
- b.HasIndex(x => new { x.OfferingId, x.CustomId }).IsUnique();
- b.HasOne(x => x.Offering).WithMany(x => x.Entitlements).HasForeignKey(x => x.OfferingId).OnDelete(DeleteBehavior.Cascade);
- }
-}
diff --git a/BTCPayServer.Data/Data/Subscriptions/FeatureData.cs b/BTCPayServer.Data/Data/Subscriptions/FeatureData.cs
new file mode 100644
index 0000000..ef228d6
--- /dev/null
+++ b/BTCPayServer.Data/Data/Subscriptions/FeatureData.cs
@@ -0,0 +1,45 @@
+#nullable enable
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace BTCPayServer.Data.Subscriptions;
+
+[Table("subs_features")]
+public class FeatureData
+{
+ /// <summary>
+ /// The internal ID of the feature, we only really use it in
+ /// SQL queries. This should not be exposed.
+ /// </summary>
+ [Required]
+ [Column("id")]
+ [Key]
+ public long Id { get; set; }
+
+ /// <summary>
+ /// The ID selected by the user, scoped at the offering level.
+ /// </summary>
+ [Required]
+ [Column("custom_id")]
+ public string CustomId { get; set; } = null!;
+ [Required]
+ [Column("offering_id")]
+ public string OfferingId { get; set; } = null!;
+
+ [ForeignKey(nameof(OfferingId))]
+ public OfferingData Offering { get; set; } = null!;
+
+ [Column("description")]
+ public string? Description { get; set; }
+
+ public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
+ {
+ var b = builder.Entity<FeatureData>();
+ b.HasKey(x => x.Id);
+ b.Property(x => x.Id).UseIdentityAlwaysColumn();
+ b.HasIndex(x => new { x.OfferingId, x.CustomId }).IsUnique();
+ b.HasOne(x => x.Offering).WithMany(x => x.Features).HasForeignKey(x => x.OfferingId).OnDelete(DeleteBehavior.Cascade);
+ }
+}
diff --git a/BTCPayServer.Data/Data/Subscriptions/OfferingData.cs b/BTCPayServer.Data/Data/Subscriptions/OfferingData.cs
index d96c268..b1fe8e7 100644
--- a/BTCPayServer.Data/Data/Subscriptions/OfferingData.cs
+++ b/BTCPayServer.Data/Data/Subscriptions/OfferingData.cs
@@ -23,7 +23,7 @@ public class OfferingData : BaseEntityData
[ForeignKey(nameof(AppId))]
public AppData App { get; set; } = null!;
- public List<EntitlementData> Entitlements { get; set; } = null!;
+ public List<FeatureData> Features { get; set; } = null!;
public List<PlanData> Plans { get; set; } = null!;
public List<SubscriberData> Subscribers { get; set; } = null!;
diff --git a/BTCPayServer.Data/Data/Subscriptions/PlanData.cs b/BTCPayServer.Data/Data/Subscriptions/PlanData.cs
index 3c43294..ce89b2c 100644
--- a/BTCPayServer.Data/Data/Subscriptions/PlanData.cs
+++ b/BTCPayServer.Data/Data/Subscriptions/PlanData.cs
@@ -114,38 +114,38 @@ public class PlanData : BaseEntityData
return (to, GracePeriodDays is 0 ? null : to.AddDays(GracePeriodDays));
}
- // Avoid cartesian explosion if there are lots of entitlements
- private List<PlanEntitlementData>? _planEntitlements;
+ // Avoid cartesian explosion if there are lots of features
+ private List<PlanFeatureData>? _planFeatures;
[NotMapped]
- public List<PlanEntitlementData> PlanEntitlements
+ public List<PlanFeatureData> PlanFeatures
{
- get => _planEntitlements ?? throw EntitlementNotLoadedException();
- set => _planEntitlements = value;
+ get => _planFeatures ?? throw FeatureNotLoadedException();
+ set => _planFeatures = value;
}
- private static InvalidOperationException EntitlementNotLoadedException()
+ private static InvalidOperationException FeatureNotLoadedException()
{
- return new InvalidOperationException("PlanEntitlements not loaded. Use ctx.PlanEntitlements.FetchPlanEntitlementsAsync() to load it");
+ return new InvalidOperationException("PlanFeatures not loaded. Use ctx.PlanFeatures.EnsureFeatureLoaded() to load it");
}
[NotMapped]
- public bool EntitlementsLoaded => _planEntitlements is not null;
+ public bool FeaturesLoaded => _planFeatures is not null;
- public Task EnsureEntitlementLoaded(ApplicationDbContext ctx) => EnsureEntitlementLoaded(ctx.Plans);
- public async Task EnsureEntitlementLoaded(DbSet<PlanData> set)
+ public Task EnsureFeatureLoaded(ApplicationDbContext ctx) => EnsureFeatureLoaded(ctx.Plans);
+ public async Task EnsureFeatureLoaded(DbSet<PlanData> set)
{
- if (!EntitlementsLoaded)
- await set.FetchPlanEntitlementsAsync(this);
+ if (!FeaturesLoaded)
+ await set.FetchPlanFeaturesAsync(this);
}
- public Task ReloadEntitlement(ApplicationDbContext ctx) => ReloadEntitlement(ctx.Plans);
- public Task ReloadEntitlement(DbSet<PlanData> set) =>set.FetchPlanEntitlementsAsync(this);
+ public Task ReloadFeature(ApplicationDbContext ctx) => ReloadFeature(ctx.Plans);
+ public Task ReloadFeature(DbSet<PlanData> set) =>set.FetchPlanFeaturesAsync(this);
- public void AssertEntitlementsLoaded() => _ = _planEntitlements ?? throw EntitlementNotLoadedException();
+ public void AssertFeaturesLoaded() => _ = _planFeatures ?? throw FeatureNotLoadedException();
- public PlanEntitlementData? GetEntitlement(long entitlementId)
- => PlanEntitlements.FirstOrDefault(p => p.EntitlementId == entitlementId);
- public PlanEntitlementData? GetEntitlement(string entitlementCustomId)
- => PlanEntitlements.FirstOrDefault(p => p.Entitlement.CustomId == entitlementCustomId);
- public string[] GetEntitlementIds()
- => PlanEntitlements.Select(p => p.Entitlement.CustomId).ToArray();
+ public PlanFeatureData? GetFeature(long featureId)
+ => PlanFeatures.FirstOrDefault(p => p.FeatureId == featureId);
+ public PlanFeatureData? GetFeature(string featureCustomId)
+ => PlanFeatures.FirstOrDefault(p => p.Feature.CustomId == featureCustomId);
+ public string[] GetFeatureIds()
+ => PlanFeatures.Select(p => p.Feature.CustomId).ToArray();
}
diff --git a/BTCPayServer.Data/Data/Subscriptions/PlanEntitlementData.cs b/BTCPayServer.Data/Data/Subscriptions/PlanEntitlementData.cs
deleted file mode 100644
index eb875e8..0000000
--- a/BTCPayServer.Data/Data/Subscriptions/PlanEntitlementData.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#nullable enable
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-
-namespace BTCPayServer.Data.Subscriptions;
-
-[Table("subs_plans_entitlements")]
-public class PlanEntitlementData
-{
- [Required]
- [Column("plan_id")]
- public string PlanId { get; set; } = null!;
-
- [ForeignKey(nameof(PlanId))]
- public PlanData Plan { get; set; } = null!;
-
- [Required]
- [Column("entitlement_id")]
- public long EntitlementId { get; set; }
-
- [ForeignKey(nameof(EntitlementId))]
- public EntitlementData Entitlement { get; set; } = null!;
-
- public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
- {
- var b = builder.Entity<PlanEntitlementData>();
- b.HasKey(o => new { o.PlanId, o.EntitlementId });
- b.HasOne(x => x.Plan).WithMany().HasForeignKey(x => x.PlanId).OnDelete(DeleteBehavior.Cascade);
- b.HasOne(x => x.Entitlement).WithMany().HasForeignKey(x => x.EntitlementId).OnDelete(DeleteBehavior.Cascade);
- }
-}
diff --git a/BTCPayServer.Data/Data/Subscriptions/PlanFeatureData.cs b/BTCPayServer.Data/Data/Subscriptions/PlanFeatureData.cs
new file mode 100644
index 0000000..d9d35eb
--- /dev/null
+++ b/BTCPayServer.Data/Data/Subscriptions/PlanFeatureData.cs
@@ -0,0 +1,33 @@
+#nullable enable
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+
+namespace BTCPayServer.Data.Subscriptions;
+
+[Table("subs_plans_features")]
+public class PlanFeatureData
+{
+ [Required]
+ [Column("plan_id")]
+ public string PlanId { get; set; } = null!;
+
+ [ForeignKey(nameof(PlanId))]
+ public PlanData Plan { get; set; } = null!;
+
+ [Required]
+ [Column("feature_id")]
+ public long FeatureId { get; set; }
+
+ [ForeignKey(nameof(FeatureId))]
+ public FeatureData Feature { get; set; } = null!;
+
+ public static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
+ {
+ var b = builder.Entity<PlanFeatureData>();
+ b.HasKey(o => new { o.PlanId, o.FeatureId });
+ b.HasOne(x => x.Plan).WithMany().HasForeignKey(x => x.PlanId).OnDelete(DeleteBehavior.Cascade);
+ b.HasOne(x => x.Feature).WithMany().HasForeignKey(x => x.FeatureId).OnDelete(DeleteBehavior.Cascade);
+ }
+}
diff --git a/BTCPayServer.Data/Migrations/20251028061727_subs.cs b/BTCPayServer.Data/Migrations/20251028061727_subs.cs
index 46c18ff..f6c80c3 100644
--- a/BTCPayServer.Data/Migrations/20251028061727_subs.cs
+++ b/BTCPayServer.Data/Migrations/20251028061727_subs.cs
@@ -87,7 +87,7 @@ namespace BTCPayServer.Migrations
});
migrationBuilder.CreateTable(
- name: "subs_entitlements",
+ name: "subs_features",
columns: table => new
{
id = table.Column<long>(type: "bigint", nullable: false)
@@ -98,9 +98,9 @@ namespace BTCPayServer.Migrations
},
constraints: table =>
{
- table.PrimaryKey("PK_subs_entitlements", x => x.id);
+ table.PrimaryKey("PK_subs_features", x => x.id);
table.ForeignKey(
- name: "FK_subs_entitlements_subs_offerings_offering_id",
+ name: "FK_subs_features_subs_offerings_offering_id",
column: x => x.offering_id,
principalTable: "subs_offerings",
principalColumn: "id",
@@ -166,23 +166,23 @@ namespace BTCPayServer.Migrations
});
migrationBuilder.CreateTable(
- name: "subs_plans_entitlements",
+ name: "subs_plans_features",
columns: table => new
{
plan_id = table.Column<string>(type: "text", nullable: false),
- entitlement_id = table.Column<long>(type: "bigint", nullable: false)
+ feature_id = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
- table.PrimaryKey("PK_subs_plans_entitlements", x => new { x.plan_id, x.entitlement_id });
+ table.PrimaryKey("PK_subs_plans_features", x => new { x.plan_id, x.feature_id });
table.ForeignKey(
- name: "FK_subs_plans_entitlements_subs_entitlements_entitlement_id",
- column: x => x.entitlement_id,
- principalTable: "subs_entitlements",
+ name: "FK_subs_plans_features_subs_features_feature_id",
+ column: x => x.feature_id,
+ principalTable: "subs_features",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
- name: "FK_subs_plans_entitlements_subs_plans_plan_id",
+ name: "FK_subs_plans_features_subs_plans_plan_id",
column: x => x.plan_id,
principalTable: "subs_plans",
principalColumn: "id",
@@ -395,8 +395,8 @@ namespace BTCPayServer.Migrations
unique: true);
migrationBuilder.CreateIndex(
- name: "IX_subs_entitlements_offering_id_custom_id",
- table: "subs_entitlements",
+ name: "IX_subs_features_offering_id_custom_id",
+ table: "subs_features",
columns: new[] { "offering_id", "custom_id" },
unique: true);
@@ -436,9 +436,9 @@ namespace BTCPayServer.Migrations
column: "offering_id");
migrationBuilder.CreateIndex(
- name: "IX_subs_plans_entitlements_entitlement_id",
- table: "subs_plans_entitlements",
- column: "entitlement_id");
+ name: "IX_subs_plans_features_feature_id",
+ table: "subs_plans_features",
+ column: "feature_id");
migrationBuilder.CreateIndex(
name: "IX_subs_portal_sessions_expiration",
@@ -518,7 +518,7 @@ namespace BTCPayServer.Migrations
name: "subs_plan_checkouts");
migrationBuilder.DropTable(
- name: "subs_plans_entitlements");
+ name: "subs_plans_features");
migrationBuilder.DropTable(
name: "subs_portal_sessions");
@@ -530,7 +530,7 @@ namespace BTCPayServer.Migrations
name: "subscriber_invoices");
migrationBuilder.DropTable(
- name: "subs_entitlements");
+ name: "subs_features");
migrationBuilder.DropTable(
name: "subs_subscriber_credits");
diff --git a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs
index 85c6a8d..08ea04e 100644
--- a/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs
+++ b/BTCPayServer.Data/Migrations/ApplicationDbContextModelSnapshot.cs
@@ -1035,7 +1035,7 @@ namespace BTCPayServer.Migrations
b.ToTable("Files");
});
- modelBuilder.Entity("BTCPayServer.Data.Subscriptions.EntitlementData", b =>
+ modelBuilder.Entity("BTCPayServer.Data.Subscriptions.FeatureData", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
@@ -1063,7 +1063,7 @@ namespace BTCPayServer.Migrations
b.HasIndex("OfferingId", "CustomId")
.IsUnique();
- b.ToTable("subs_entitlements");
+ b.ToTable("subs_features");
});
modelBuilder.Entity("BTCPayServer.Data.Subscriptions.OfferingData", b =>
@@ -1347,21 +1347,21 @@ namespace BTCPayServer.Migrations
b.ToTable("subs_plans");
});
- modelBuilder.Entity("BTCPayServer.Data.Subscriptions.PlanEntitlementData", b =>
+ modelBuilder.Entity("BTCPayServer.Data.Subscriptions.PlanFeatureData", b =>
{
b.Property<string>("PlanId")
.HasColumnType("text")
.HasColumnName("plan_id");
- b.Property<long>("EntitlementId")
+ b.Property<long>("FeatureId")
.HasColumnType("bigint")
- .HasColumnName("entitlement_id");
+ .HasColumnName("feature_id");
- b.HasKey("PlanId", "EntitlementId");
+ b.HasKey("PlanId", "FeatureId");
- b.HasIndex("EntitlementId");
+ b.HasIndex("FeatureId");
- b.ToTable("subs_plans_entitlements");
+ b.ToTable("subs_plans_features");
});
modelBuilder.Entity("BTCPayServer.Data.Subscriptions.PortalSessionData", b =>
@@ -2246,10 +2246,10 @@ namespace BTCPayServer.Migrations
b.Navigation("ApplicationUser");
});
- modelBuilder.Entity("BTCPayServer.Data.Subscriptions.EntitlementData", b =>
+ modelBuilder.Entity("BTCPayServer.Data.Subscriptions.FeatureData", b =>
{
b.HasOne("BTCPayServer.Data.Subscriptions.OfferingData", "Offering")
- .WithMany("Entitlements")
+ .WithMany("Features")
.HasForeignKey("OfferingId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -2323,11 +2323,11 @@ namespace BTCPayServer.Migrations
b.Navigation("Offering");
});
- modelBuilder.Entity("BTCPayServer.Data.Subscriptions.PlanEntitlementData", b =>
+ modelBuilder.Entity("BTCPayServer.Data.Subscriptions.PlanFeatureData", b =>
{
- b.HasOne("BTCPayServer.Data.Subscriptions.EntitlementData", "Entitlement")
+ b.HasOne("BTCPayServer.Data.Subscriptions.FeatureData", "Feature")
.WithMany()
- .HasForeignKey("EntitlementId")
+ .HasForeignKey("FeatureId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
@@ -2337,7 +2337,7 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.Navigation("Entitlement");
+ b.Navigation("Feature");
b.Navigation("Plan");
});
@@ -2631,7 +2631,7 @@ namespace BTCPayServer.Migrations
modelBuilder.Entity("BTCPayServer.Data.Subscriptions.OfferingData", b =>
{
- b.Navigation("Entitlements");
+ b.Navigation("Features");
b.Navigation("Plans");
diff --git a/BTCPayServer.Tests/MonetizationTests.cs b/BTCPayServer.Tests/MonetizationTests.cs
index 090040e..84b2950 100644
--- a/BTCPayServer.Tests/MonetizationTests.cs
+++ b/BTCPayServer.Tests/MonetizationTests.cs
@@ -139,7 +139,7 @@ public class MonetizationTests(ITestOutputHelper helper) : UnitTestBase(helper)
// Now, the admin decides to remove can-access form plan. This should cut off access
await offeringPMO.GoToPlans();
var edit = await offeringPMO.Edit("Starter Plan");
- edit.DisableEntitlements = ["can-access"];
+ edit.DisableFeatures = ["can-access"];
var lockoutUpdated = await s.Server.WaitForEvent<MonetizationHostedService.MonetizationLockoutUpdated>(async () =>
{
@@ -163,7 +163,7 @@ public class MonetizationTests(ITestOutputHelper helper) : UnitTestBase(helper)
var add = await offeringPMO.AddPlan();
add.PlanName = "Pro Plan";
add.Price = "100";
- add.EnableEntitlements = ["can-access"];
+ add.EnableFeatures = ["can-access"];
await add.Save();
edit = await offeringPMO.Edit("Starter Plan");
edit.PlanChanges = [SubscriptionTests.AddEditPlanPMO.PlanChangeType.Upgrade];
diff --git a/BTCPayServer.Tests/SubscriptionTests.cs b/BTCPayServer.Tests/SubscriptionTests.cs
index a778163..f4b8b3b 100644
--- a/BTCPayServer.Tests/SubscriptionTests.cs
+++ b/BTCPayServer.Tests/SubscriptionTests.cs
@@ -64,7 +64,7 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
Price = "10.00",
TrialPeriod = "7",
GracePeriod = "7",
- EnableEntitlements = ["transaction-limit-10000", "payment-processing-0", "email-support-0"],
+ EnableFeatures = ["transaction-limit-10000", "payment-processing-0", "email-support-0"],
PlanChanges = [AddEditPlanPMO.PlanChangeType.Upgrade, AddEditPlanPMO.PlanChangeType.Downgrade]
};
await offeringPMO.AddPlan();
@@ -87,8 +87,8 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
editPlan.GracePeriod = "5";
editPlan.Description = "Super cool plan";
editPlan.OptimisticActivation = true;
- editPlan.EnableEntitlements = ["transaction-limit-50000", "payment-processing-1", "email-support-1"];
- editPlan.DisableEntitlements = ["transaction-limit-10000", "payment-processing-0", "email-support-0"];
+ editPlan.EnableFeatures = ["transaction-limit-50000", "payment-processing-1", "email-support-1"];
+ editPlan.DisableFeatures = ["transaction-limit-10000", "payment-processing-0", "email-support-0"];
await editPlan.Save();
await s.Page.GetByRole(AriaRole.Link, new() { Name = "Edit" }).ClickAsync();
@@ -98,7 +98,7 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
editPlan = new AddEditPlanPMO(s);
await editPlan.ReadFields();
- editPlan.DisableEntitlements = null;
+ editPlan.DisableFeatures = null;
expected.AssertEqual(editPlan);
await s.Page.GetByTestId("offering-link").ClickAsync();
await offeringPMO.Configure();
@@ -107,34 +107,34 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
{
Name = "New test offering 2",
SuccessRedirectUrl = "https://test.com/test",
- Entitlements_0__Id = "analytics-dashboard-0-2",
- Entitlements_0__ShortDescription = "Basic analytics dashboard 2",
+ Features_0__Id = "analytics-dashboard-0-2",
+ Features_0__ShortDescription = "Basic analytics dashboard 2",
};
await configureOffering.Fill();
// Remove "analytics-dashboard-1" which is the second item
- Assert.Equal("analytics-dashboard-1", await s.Page.Locator("#Entitlements_1__Id").InputValueAsync());
+ Assert.Equal("analytics-dashboard-1", await s.Page.Locator("#Features_1__Id").InputValueAsync());
await s.Page.Locator("button[name='removeIndex']").Nth(1).ClickAsync();
await s.ClickPagePrimary();
await offeringPMO.Configure();
var expectedConfigure = configureOffering;
- expectedConfigure.Entitlements_1__Id = "analytics-dashboard-x";
- expectedConfigure.Entitlements_1__ShortDescription = "Custom analytics & reporting";
+ expectedConfigure.Features_1__Id = "analytics-dashboard-x";
+ expectedConfigure.Features_1__ShortDescription = "Custom analytics & reporting";
configureOffering = new ConfigureOfferingPMO(s);
await configureOffering.ReadFields();
expectedConfigure.AssertEqual(configureOffering);
// Can we add "Support" back?
await s.Page.GetByRole(AriaRole.Button, new() { Name = "Add item" }).ClickAsync();
- await s.Page.Locator("#Entitlements_14__Id").FillAsync("analytics-dashboard-1");
- await s.Page.Locator("#Entitlements_14__ShortDescription").FillAsync("Advanced analytics");
+ await s.Page.Locator("#Features_14__Id").FillAsync("analytics-dashboard-1");
+ await s.Page.Locator("#Features_14__ShortDescription").FillAsync("Advanced analytics");
await s.ClickPagePrimary();
await offeringPMO.Configure();
- expectedConfigure.Entitlements_1__Id = "analytics-dashboard-1";
- expectedConfigure.Entitlements_1__ShortDescription = "Advanced analytics";
+ expectedConfigure.Features_1__Id = "analytics-dashboard-1";
+ expectedConfigure.Features_1__ShortDescription = "Advanced analytics";
configureOffering = new ConfigureOfferingPMO(s);
await configureOffering.ReadFields();
expectedConfigure.AssertEqual(configureOffering);
@@ -819,10 +819,10 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
public string? Name { get; set; }
public string? SuccessRedirectUrl { get; set; }
- public string? Entitlements_0__Id { get; set; }
- public string? Entitlements_0__ShortDescription { get; set; }
- public string? Entitlements_1__Id { get; set; }
- public string? Entitlements_1__ShortDescription { get; set; }
+ public string? Features_0__Id { get; set; }
+ public string? Features_0__ShortDescription { get; set; }
+ public string? Features_1__Id { get; set; }
+ public string? Features_1__ShortDescription { get; set; }
public async Task Fill()
{
@@ -831,14 +831,14 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
await s.Page.Locator("#Name").FillAsync(Name);
if (SuccessRedirectUrl is not null)
await s.Page.GetByRole(AriaRole.Textbox, new() { Name = "Success redirect url" }).FillAsync(SuccessRedirectUrl);
- if (Entitlements_0__Id is not null)
- await s.Page.Locator("#Entitlements_0__Id").FillAsync(Entitlements_0__Id);
- if (Entitlements_0__ShortDescription is not null)
- await s.Page.Locator("#Entitlements_0__ShortDescription").FillAsync(Entitlements_0__ShortDescription);
- if (Entitlements_1__Id is not null)
- await s.Page.Locator("#Entitlements_1__Id").FillAsync(Entitlements_1__Id);
- if (Entitlements_1__ShortDescription is not null)
- await s.Page.Locator("#Entitlements_1__ShortDescription").FillAsync(Entitlements_1__ShortDescription);
+ if (Features_0__Id is not null)
+ await s.Page.Locator("#Features_0__Id").FillAsync(Features_0__Id);
+ if (Features_0__ShortDescription is not null)
+ await s.Page.Locator("#Features_0__ShortDescription").FillAsync(Features_0__ShortDescription);
+ if (Features_1__Id is not null)
+ await s.Page.Locator("#Features_1__Id").FillAsync(Features_1__Id);
+ if (Features_1__ShortDescription is not null)
+ await s.Page.Locator("#Features_1__ShortDescription").FillAsync(Features_1__ShortDescription);
}
public async Task ReadFields()
@@ -846,20 +846,20 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
var s = tester;
Name = await s.Page.Locator("#Name").InputValueAsync();
SuccessRedirectUrl = await s.Page.GetByRole(AriaRole.Textbox, new() { Name = "Success redirect url" }).InputValueAsync();
- Entitlements_0__Id = await s.Page.Locator("#Entitlements_0__Id").InputValueAsync();
- Entitlements_0__ShortDescription = await s.Page.Locator("#Entitlements_0__ShortDescription").InputValueAsync();
- Entitlements_1__Id = await s.Page.Locator("#Entitlements_1__Id").InputValueAsync();
- Entitlements_1__ShortDescription = await s.Page.Locator("#Entitlements_1__ShortDescription").InputValueAsync();
+ Features_0__Id = await s.Page.Locator("#Features_0__Id").InputValueAsync();
+ Features_0__ShortDescription = await s.Page.Locator("#Features_0__ShortDescription").InputValueAsync();
+ Features_1__Id = await s.Page.Locator("#Features_1__Id").InputValueAsync();
+ Features_1__ShortDescription = await s.Page.Locator("#Features_1__ShortDescription").InputValueAsync();
}
public void AssertEqual(ConfigureOfferingPMO b)
{
Assert.Equal(Name ?? "", b.Name ?? "");
Assert.Equal(SuccessRedirectUrl ?? "", b.SuccessRedirectUrl ?? "");
- Assert.Equal(Entitlements_0__Id ?? "", b.Entitlements_0__Id ?? "");
- Assert.Equal(Entitlements_0__ShortDescription ?? "", b.Entitlements_0__ShortDescription ?? "");
- Assert.Equal(Entitlements_1__Id ?? "", b.Entitlements_1__Id ?? "");
- Assert.Equal(Entitlements_1__ShortDescription ?? "", b.Entitlements_1__ShortDescription ?? "");
+ Assert.Equal(Features_0__Id ?? "", b.Features_0__Id ?? "");
+ Assert.Equal(Features_0__ShortDescription ?? "", b.Features_0__ShortDescription ?? "");
+ Assert.Equal(Features_1__Id ?? "", b.Features_1__Id ?? "");
+ Assert.Equal(Features_1__ShortDescription ?? "", b.Features_1__ShortDescription ?? "");
}
}
@@ -872,8 +872,8 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
public string? Description { get; set; }
public bool? OptimisticActivation { get; set; }
- public List<string>? EnableEntitlements { get; set; }
- public List<string>? DisableEntitlements { get; set; }
+ public List<string>? EnableFeatures { get; set; }
+ public List<string>? DisableFeatures { get; set; }
public PlanChangeType[]? PlanChanges { get; set; }
public bool? Renewable { get; set; }
@@ -906,14 +906,14 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
}
}
- foreach (var entitlement in EnableEntitlements ?? [])
+ foreach (var feature in EnableFeatures ?? [])
{
- await s.Page.GetByTestId($"check_{entitlement}").CheckAsync();
+ await s.Page.GetByTestId($"check_{feature}").CheckAsync();
}
- foreach (var entitlement in DisableEntitlements ?? [])
+ foreach (var feature in DisableFeatures ?? [])
{
- await s.Page.GetByTestId($"check_{entitlement}").UncheckAsync();
+ await s.Page.GetByTestId($"check_{feature}").UncheckAsync();
}
if (OptimisticActivation is not null)
@@ -933,19 +933,19 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
TrialPeriod = await s.Page.GetByRole(AriaRole.Spinbutton, new() { Name = "Trial Period (days)" }).InputValueAsync();
GracePeriod = await s.Page.GetByRole(AriaRole.Spinbutton, new() { Name = "Grace Period (days)" }).InputValueAsync();
- foreach (var entitlement in await s.Page.QuerySelectorAllAsync(".entitlement-checkbox"))
+ foreach (var feature in await s.Page.QuerySelectorAllAsync(".feature-checkbox"))
{
- var isChecked = await entitlement.IsCheckedAsync();
- var id = (await entitlement.GetAttributeAsync("data-testid"))!.Substring(6);
+ var isChecked = await feature.IsCheckedAsync();
+ var id = (await feature.GetAttributeAsync("data-testid"))!.Substring(6);
if (isChecked)
{
- EnableEntitlements ??= new();
- EnableEntitlements.Add(id);
+ EnableFeatures ??= new();
+ EnableFeatures.Add(id);
}
else
{
- DisableEntitlements ??= new();
- DisableEntitlements.Add(id);
+ DisableFeatures ??= new();
+ DisableFeatures.Add(id);
}
}
@@ -968,20 +968,20 @@ public class SubscriptionTests(ITestOutputHelper testOutputHelper) : UnitTestBas
Assert.Equal(TrialPeriod ?? "", b.TrialPeriod ?? "");
Assert.Equal(GracePeriod ?? "", b.GracePeriod ?? "");
- if (EnableEntitlements is not null && b.EnableEntitlements is not null)
+ if (EnableFeatures is not null && b.EnableFeatures is not null)
{
- Assert.Equal(EnableEntitlements.Count, b.EnableEntitlements.Count);
+ Assert.Equal(EnableFeatures.Count, b.EnableFeatures.Count);
- var (ea, eb) = (EnableEntitlements.OrderBy(e => e).ToArray(), b.EnableEntitlements.OrderBy(e => e).ToArray());
- for (int i = 0; i < EnableEntitlements.Count; i++)
+ var (ea, eb) = (EnableFeatures.OrderBy(e => e).ToArray(), b.EnableFeatures.OrderBy(e => e).ToArray());
+ for (int i = 0; i < EnableFeatures.Count; i++)
Assert.Equal(ea[i], eb[i]);
}
- if (DisableEntitlements is not null && b.DisableEntitlements is not null)
+ if (DisableFeatures is not null && b.DisableFeatures is not null)
{
- Assert.Equal(DisableEntitlements.Count, b.DisableEntitlements.Count);
- var (ea, eb) = (DisableEntitlements.OrderBy(e => e).ToArray(), b.DisableEntitlements.OrderBy(e => e).ToArray());
- for (int i = 0; i < DisableEntitlements.Count; i++)
+ Assert.Equal(DisableFeatures.Count, b.DisableFeatures.Count);
+ var (ea, eb) = (DisableFeatures.OrderBy(e => e).ToArray(), b.DisableFeatures.OrderBy(e => e).ToArray());
+ for (int i = 0; i < DisableFeatures.Count; i++)
Assert.Equal(ea[i], eb[i]);
}
diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs
index dcec3d9..1431ad5 100644
--- a/BTCPayServer.Tests/UnitTest1.cs
+++ b/BTCPayServer.Tests/UnitTest1.cs
@@ -78,6 +78,7 @@ using Microsoft.Extensions.Caching.Memory;
using PosViewType = BTCPayServer.Client.Models.PosViewType;
using BTCPayServer.Plugins.Emails.Controllers;
using BTCPayServer.Views.Stores;
+using Microsoft.Playwright;
using MimeKit;
using NBXplorer.DerivationStrategy;
@@ -1424,6 +1425,7 @@ namespace BTCPayServer.Tests
await tester.Page.FillAsync("#Spread", "10");
await Test("BTC_JPY,BTC_CAD");
+ await tester.Page.WaitForLoadStateAsync(LoadState.DOMContentLoaded);
var rules = await tester.Page.Locator(".testresult .testresult_rule").AllAsync();
if (fallback)
{
diff --git a/BTCPayServer/Plugins/Monetization/Controllers/UIServerMonetizationController.cs b/BTCPayServer/Plugins/Monetization/Controllers/UIServerMonetizationController.cs
index 96e59bf..68d9ca5 100644
--- a/BTCPayServer/Plugins/Monetization/Controllers/UIServerMonetizationController.cs
+++ b/BTCPayServer/Plugins/Monetization/Controllers/UIServerMonetizationController.cs
@@ -77,8 +77,8 @@ public class UIServerMonetizationController(
if (vm.Offering is not null)
{
var planIds = activePlans.Select(p => p.Id).Distinct().ToArray();
- canLogin = (await ctx.PlanEntitlements.Where(p => planIds.Contains(p.PlanId))
- .Where(o => o.Entitlement.CustomId == MonetizationEntitlements.CanAccess)
+ canLogin = (await ctx.PlanFeatures.Where(p => planIds.Contains(p.PlanId))
+ .Where(o => o.Feature.CustomId == MonetizationFeatures.CanAccess)
.Select(o => o.PlanId)
.ToArrayAsync()).ToHashSet();
}
@@ -167,10 +167,10 @@ public class UIServerMonetizationController(
return await Monetization();
var (_, offeringId) = await appService.CreateOffering(selectedStore, "BTCPay Server Access");
- var entitlements = CreateDefaultEntitlements(offeringId);
- foreach (var e in entitlements.Values)
+ var features = CreateDefaultFeatures(offeringId);
+ foreach (var e in features.Values)
{
- ctx.Entitlements.Add(e);
+ ctx.Features.Add(e);
}
var currency = store.GetStoreBlob().DefaultCurrency;
@@ -186,12 +186,12 @@ public class UIServerMonetizationController(
OfferingId = offeringId,
};
ctx.Plans.Add(starterPlan);
- ctx.PlanEntitlements.AddRange(
- new[] { MonetizationEntitlements.CanAccess }
- .Select(e => new PlanEntitlementData()
+ ctx.PlanFeatures.AddRange(
+ new[] { MonetizationFeatures.CanAccess }
+ .Select(e => new PlanFeatureData()
{
Plan = starterPlan,
- Entitlement = entitlements[e],
+ Feature = features[e],
}));
var serverBase = Request.GetRequestBaseUrl().ToString();
if (store.StoreWebsite != serverBase)
@@ -361,17 +361,17 @@ public class UIServerMonetizationController(
await settingsRepository.UpdateSetting(policies);
}
- private Dictionary<string, EntitlementData> CreateDefaultEntitlements(string offeringId)
+ private Dictionary<string, FeatureData> CreateDefaultFeatures(string offeringId)
{
- var entitlements = new[]
+ var features = new[]
{
- (MonetizationEntitlements.CanAccess, StringLocalizer["Can access BTCPay Server"].Value),
- }.Select(e => new EntitlementData()
+ (MonetizationFeatures.CanAccess, StringLocalizer["Can access BTCPay Server"].Value),
+ }.Select(e => new FeatureData()
{
CustomId = e.Item1,
Description = e.Item2,
OfferingId = offeringId,
}).ToDictionary(e => e.CustomId, e => e);
- return entitlements;
+ return features;
}
}
diff --git a/BTCPayServer/Plugins/Monetization/Controllers/UIUserMonetizationController.cs b/BTCPayServer/Plugins/Monetization/Controllers/UIUserMonetizationController.cs
index 91b1e1b..630f441 100644
--- a/BTCPayServer/Plugins/Monetization/Controllers/UIUserMonetizationController.cs
+++ b/BTCPayServer/Plugins/Monetization/Controllers/UIUserMonetizationController.cs
@@ -65,7 +65,7 @@ public class UIUserMonetizationController(
if (fastRedirect)
{
if (sub is { PlanId: { } planId } &&
- !await ctx.Plans.HasEntitlements(planId, MonetizationEntitlements.CanAccess))
+ !await ctx.Plans.HasFeature(planId, MonetizationFeatures.CanAccess))
{
var planChanges =
await ctx.PlanChanges
diff --git a/BTCPayServer/Plugins/Monetization/MonetizationEntitlements.cs b/BTCPayServer/Plugins/Monetization/MonetizationEntitlements.cs
deleted file mode 100644
index 1bd5885..0000000
--- a/BTCPayServer/Plugins/Monetization/MonetizationEntitlements.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace BTCPayServer.Plugins.Monetization;
-
-public class MonetizationEntitlements
-{
- public const string CanAccess = "can-access";
-}
diff --git a/BTCPayServer/Plugins/Monetization/MonetizationFeatures.cs b/BTCPayServer/Plugins/Monetization/MonetizationFeatures.cs
new file mode 100644
index 0000000..52e3973
--- /dev/null
+++ b/BTCPayServer/Plugins/Monetization/MonetizationFeatures.cs
@@ -0,0 +1,6 @@
+namespace BTCPayServer.Plugins.Monetization;
+
+public class MonetizationFeatures
+{
+ public const string CanAccess = "can-access";
+}
diff --git a/BTCPayServer/Plugins/Monetization/MonetizationHostedService.cs b/BTCPayServer/Plugins/Monetization/MonetizationHostedService.cs
index a6865ca..6167859 100644
--- a/BTCPayServer/Plugins/Monetization/MonetizationHostedService.cs
+++ b/BTCPayServer/Plugins/Monetization/MonetizationHostedService.cs
@@ -87,7 +87,7 @@ public class MonetizationHostedService(
if (evt is SubscriptionEvent.PlanStarted ps && ps.PreviousPlan.Id != ps.Subscriber.Plan.Id)
{
await using var ctx = dbContextFactory.CreateContext();
- var canAccess = await ctx.Plans.HasEntitlements(ps.Subscriber.PlanId, MonetizationEntitlements.CanAccess);
+ var canAccess = await ctx.Plans.HasFeature(ps.Subscriber.PlanId, MonetizationFeatures.CanAccess);
await UpdateUserLockout(ps, userManager, canAccess);
}
@@ -291,7 +291,7 @@ public class MonetizationHostedService(
userIds = await GetUserIdsInPlan(ctx, plan);
if (userIds.Length == 0)
return;
- var canAccess = await ctx.Plans.HasEntitlements(plan.Id, MonetizationEntitlements.CanAccess);
+ var canAccess = await ctx.Plans.HasFeature(plan.Id, MonetizationFeatures.CanAccess);
var updated = (await ctx.Database.GetDbConnection()
.QueryAsync<(string UserId, bool LockoutEnabled)>("""
WITH
diff --git a/BTCPayServer/Plugins/Monetization/MonetizationLoginExtension.cs b/BTCPayServer/Plugins/Monetization/MonetizationLoginExtension.cs
index 6d808c5..b786e9a 100644
--- a/BTCPayServer/Plugins/Monetization/MonetizationLoginExtension.cs
+++ b/BTCPayServer/Plugins/Monetization/MonetizationLoginExtension.cs
@@ -37,7 +37,7 @@ public class MonetizationLoginExtension(
// The subscriber is in a plan without an access feature
if (subscriber is { PlanId: { } planId } &&
- !await ctx.Plans.HasEntitlements(planId, MonetizationEntitlements.CanAccess))
+ !await ctx.Plans.HasFeature(planId, MonetizationFeatures.CanAccess))
{
context.Failures.Add(new (context.StringLocalizer["Your plan does not allow you to log in."]));
if (await CanChangePlan(ctx, planId))
diff --git a/BTCPayServer/Plugins/Monetization/Views/MigrateUsersModal.cshtml b/BTCPayServer/Plugins/Monetization/Views/MigrateUsersModal.cshtml
index 5d935f1..a7633d5 100644
--- a/BTCPayServer/Plugins/Monetization/Views/MigrateUsersModal.cshtml
+++ b/BTCPayServer/Plugins/Monetization/Views/MigrateUsersModal.cshtml
@@ -11,7 +11,7 @@
</div>
<form method="post">
<div class="modal-body">
- <p>@ViewLocalizer["By proceeding, all non-admin users will be migrated to the selected plan. If the plan does not include <b>can-access</b> entitlement, the user accounts will be disabled."]</p>
+ <p>@ViewLocalizer["By proceeding, all non-admin users will be migrated to the selected plan. If the plan does not include the <b>can-access</b> feature, the user accounts will be disabled."]</p>
<div class="form-group">
<label asp-for="SelectedPlanId" class="form-label" text-translate="true">Select plan</label>
<select class="form-select" asp-items="Model.AvailablePlans" asp-for="SelectedPlanId"></select>
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.Cheating.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.Cheating.cs
index a35e3de..ee4c978 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.Cheating.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.Cheating.cs
@@ -53,7 +53,7 @@ public partial class UIOfferingController
("Custom analytics & reporting", "analytics-dashboard-x"),
})
{
- ctx.Entitlements.Add(new()
+ ctx.Features.Add(new()
{
OfferingId = offering.Id,
Description = e.Item1,
@@ -62,7 +62,7 @@ public partial class UIOfferingController
}
await ctx.SaveChangesAsync();
- var entitlements = await ctx.Entitlements.Where(c => c.OfferingId == offeringId).ToDictionaryAsync(x => x.CustomId);
+ var features = await ctx.Features.Where(c => c.OfferingId == offeringId).ToDictionaryAsync(x => x.CustomId);
var p = ctx.Plans.Add(new()
{
@@ -85,10 +85,10 @@ public partial class UIOfferingController
"analytics-dashboard-0"
})
{
- ctx.PlanEntitlements.Add(new()
+ ctx.PlanFeatures.Add(new()
{
PlanId = p.Entity.Id,
- EntitlementId = entitlements[e].Id
+ FeatureId = features[e].Id
});
}
@@ -113,10 +113,10 @@ public partial class UIOfferingController
"analytics-dashboard-1"
})
{
- ctx.PlanEntitlements.Add(new()
+ ctx.PlanFeatures.Add(new()
{
PlanId = p.Entity.Id,
- EntitlementId = entitlements[e].Id
+ FeatureId = features[e].Id
});
}
@@ -142,10 +142,10 @@ public partial class UIOfferingController
"analytics-dashboard-x"
})
{
- ctx.PlanEntitlements.Add(new()
+ ctx.PlanFeatures.Add(new()
{
PlanId = p.Entity.Id,
- EntitlementId = entitlements[e].Id
+ FeatureId = features[e].Id
});
}
diff --git a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
index 06f192d..8d3ac6d 100644
--- a/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Controllers/UIOfferingController.cs
@@ -372,20 +372,20 @@ public partial class UIOfferingController(
bool itemsUpdated = false;
if (command == "AddItem")
{
- vm.Entitlements ??= new();
- vm.Entitlements.Add(new());
+ vm.Features ??= new();
+ vm.Features.Add(new());
itemsUpdated = true;
}
else if (removeIndex is int i)
{
- vm.Entitlements.RemoveAt(i);
+ vm.Features.RemoveAt(i);
itemsUpdated = true;
}
if (itemsUpdated)
{
this.ModelState.Clear();
- vm.Anchor = "entitlements";
+ vm.Anchor = "features";
}
if (!ModelState.IsValid || itemsUpdated)
@@ -394,31 +394,31 @@ public partial class UIOfferingController(
offering.SuccessRedirectUrl = vm.SuccessRedirectUrl;
offering.App.Name = vm.Name;
- UpdateEntitlements(ctx, offering, vm);
+ UpdateFeatures(ctx, offering, vm);
await ctx.SaveChangesAsync();
this.TempData.SetStatusSuccess(StringLocalizer["Offering configuration updated"]);
return GoToOffering(storeId, offeringId);
}
- private static void UpdateEntitlements(ApplicationDbContext ctx, OfferingData offering, ConfigureOfferingViewModel vm)
+ private static void UpdateFeatures(ApplicationDbContext ctx, OfferingData offering, ConfigureOfferingViewModel vm)
{
- var incomingById = vm.Entitlements
+ var incomingById = vm.Features
.GroupBy(e => e.Id) // guard against dupes
.ToDictionary(g => g.Key, g => g.First());
- var existingById = offering.Entitlements
+ var existingById = offering.Features
.ToDictionary(e => e.CustomId);
- var toRemove = offering.Entitlements
+ var toRemove = offering.Features
.Where(e => !incomingById.ContainsKey(e.CustomId))
.ToList();
foreach (var e in toRemove)
- offering.Entitlements.Remove(e);
+ offering.Features.Remove(e);
- ctx.Entitlements.RemoveRange(toRemove);
+ ctx.Features.RemoveRange(toRemove);
foreach (var (id, vmEnt) in incomingById)
{
@@ -427,7 +427,7 @@ public partial class UIOfferingController(
entity = new();
entity.CustomId = vmEnt.Id;
entity.OfferingId = offering.Id;
- offering.Entitlements.Add(entity);
+ offering.Features.Add(entity);
}
entity.Description = vmEnt.ShortDescription;
@@ -500,11 +500,11 @@ public partial class UIOfferingController(
})
.OrderBy(p => p.PlanName)
.ToList(),
- Entitlements = offering.Entitlements.OrderBy(e => e.CustomId).Select(e => new AddEditPlanViewModel.Entitlement()
+ Features = offering.Features.OrderBy(e => e.CustomId).Select(e => new AddEditPlanViewModel.Feature()
{
CustomId = e.CustomId,
ShortDescription = e.Description,
- Selected = plan?.GetEntitlement(e.Id) is not null
+ Selected = plan?.GetFeature(e.Id) is not null
}).ToList(),
};
@@ -532,7 +532,7 @@ public partial class UIOfferingController(
plan ??= new PlanData()
{
CreatedAt = DateTimeOffset.UtcNow,
- PlanEntitlements = new()
+ PlanFeatures = new()
};
plan.Name = vm.Name;
plan.Description = vm.Description;
@@ -584,17 +584,17 @@ public partial class UIOfferingController(
await ctx.SaveChangesAsync();
- var customIdsToIds = offering.Entitlements.ToDictionary(x => x.CustomId, x => x.Id);
- var enabled = vm.Entitlements.Where(e => e.Selected).Select(e => customIdsToIds[e.CustomId]).ToArray();
+ var customIdsToIds = offering.Features.ToDictionary(x => x.CustomId, x => x.Id);
+ var enabled = vm.Features.Where(e => e.Selected).Select(e => customIdsToIds[e.CustomId]).ToArray();
await ctx.Database.GetDbConnection()
.ExecuteAsync("""
- DELETE FROM subs_plans_entitlements
- WHERE plan_id = @planId AND NOT (entitlement_id = ANY(@enabled));
- INSERT INTO subs_plans_entitlements(plan_id, entitlement_id)
+ DELETE FROM subs_plans_features
+ WHERE plan_id = @planId AND NOT (feature_id = ANY(@enabled));
+ INSERT INTO subs_plans_features(plan_id, feature_id)
SELECT @planId, e FROM unnest(@enabled) e
ON CONFLICT DO NOTHING;
""", new { planId = plan.Id, enabled });
- await plan.ReloadEntitlement(ctx);
+ await plan.ReloadFeature(ctx);
if (planId is null)
this.TempData.SetStatusSuccess(StringLocalizer["New plan created"]);
else
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs b/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
index 1409a5c..68beb17 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriberWebhookProvider.cs
@@ -175,7 +175,7 @@ public class SubscriberWebhookProvider : WebhookTriggerProvider<SubscriptionEven
Description = plan.Description,
MemberCount = plan.MemberCount,
OptimisticActivation = plan.OptimisticActivation,
- Entitlements = plan.GetEntitlementIds()
+ Features = plan.GetFeatureIds()
},
PeriodEnd = sub.PeriodEnd,
TrialEnd = sub.TrialEnd,
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionEvent.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionEvent.cs
index db1d52d..ec1ce43 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriptionEvent.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionEvent.cs
@@ -58,7 +58,7 @@ public class SubscriptionEvent
public PlanUpdated(PlanData plan)
{
Plan = plan;
- plan.AssertEntitlementsLoaded();
+ plan.AssertFeaturesLoaded();
}
public PlanData Plan { get; set; }
diff --git a/BTCPayServer/Plugins/Subscriptions/SubscriptionHostedService.cs b/BTCPayServer/Plugins/Subscriptions/SubscriptionHostedService.cs
index 70af428..2e5bdf7 100644
--- a/BTCPayServer/Plugins/Subscriptions/SubscriptionHostedService.cs
+++ b/BTCPayServer/Plugins/Subscriptions/SubscriptionHostedService.cs
@@ -243,7 +243,7 @@ public class SubscriptionHostedService(
var (now, ctx, cancellationToken) = (subCtx.Now, subCtx.Context, subCtx.CancellationToken);
var query = ctx.Subscribers.IncludeAll();
var members = await selector.Where(query).ToListAsync(cancellationToken);
- await ctx.PlanEntitlements.FetchPlanEntitlementsAsync(members.Select(m => m.Plan));
+ await ctx.PlanFeatures.FetchPlanFeaturesAsync(members.Select(m => m.Plan));
foreach (var m in members)
{
var newPhase = m.GetExpectedPhase(now);
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddEditPlanViewModel.cs b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddEditPlanViewModel.cs
index 5ba589a..9ba1947 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddEditPlanViewModel.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddEditPlanViewModel.cs
@@ -50,8 +50,8 @@ namespace BTCPayServer.Views.UIStoreMembership
[Display(Name = "Renewable")]
public bool Renewable { get; set; } = true;
- [Display(Name = "Entitlements")]
- public List<Entitlement> Entitlements { get; set; } = new();
+ [Display(Name = "Features")]
+ public List<Feature> Features { get; set; } = new();
public string Anchor { get; set; }
public string PlanId { get; set; }
@@ -64,7 +64,7 @@ namespace BTCPayServer.Views.UIStoreMembership
public string SelectedType { get; set; }
}
- public class Entitlement
+ public class Feature
{
public string CustomId { get; set; } = string.Empty;
public string ShortDescription { get; set; } = string.Empty;
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddPlan.cshtml b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddPlan.cshtml
index 82fa7e6..06f41f6 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddPlan.cshtml
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/AddPlan.cshtml
@@ -102,7 +102,7 @@
</div>
@if (Model.PlanChanges?.Any() is true)
{
- <section id="entitlements" class="mt-4">
+ <section id="plan-changes" class="mt-4">
<h4 class="mb-4">Plan changes</h4>
<p>Allow the subscriber to downgrade or upgrade to a different plan.</p>
<div class="table-responsive">
@@ -133,10 +133,10 @@
</section>
}
- @if (Model.Entitlements?.Any() is true)
+ @if (Model.Features?.Any() is true)
{
- <section id="entitlements" class="mt-4">
- <h4 class="mb-4" text-translate="true">Entitlements</h4>
+ <section id="features" class="mt-4">
+ <h4 class="mb-4" text-translate="true">Features</h4>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
@@ -147,17 +147,17 @@
</tr>
</thead>
<tbody>
- @for (int i = 0; i < Model.Entitlements.Count(); i++)
+ @for (int i = 0; i < Model.Features.Count(); i++)
{
<tr>
<td>
- <input asp-for="Entitlements[i].Selected" class="form-check-input entitlement-checkbox"
- data-testid="check_@Model.Entitlements[i].CustomId" type="checkbox">
- <input asp-for="Entitlements[i].CustomId" type="hidden">
- <input asp-for="Entitlements[i].ShortDescription" type="hidden">
+ <input asp-for="Features[i].Selected" class="form-check-input feature-checkbox"
+ data-testid="check_@Model.Features[i].CustomId" type="checkbox">
+ <input asp-for="Features[i].CustomId" type="hidden">
+ <input asp-for="Features[i].ShortDescription" type="hidden">
</td>
- <td>@Model.Entitlements[i].CustomId</td>
- <td>@Model.Entitlements[i].ShortDescription</td>
+ <td>@Model.Features[i].CustomId</td>
+ <td>@Model.Features[i].ShortDescription</td>
</tr>
}
</tbody>
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOffering.cshtml b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOffering.cshtml
index 78b2a94..fd9df08 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOffering.cshtml
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOffering.cshtml
@@ -56,13 +56,13 @@
</div>
- <section id="entitlements" class="mt-4">
+ <section id="features" class="mt-4">
<div class="d-flex align-items-center gap-4">
- <h4 text-translate="true">Entitlements</h4>
+ <h4 text-translate="true">Features</h4>
<button type="submit" name="command" value="AddItem" class="btn btn-outline-secondary" id="AddPlanItem">Add item</button>
</div>
- @if (Model.Entitlements?.Any() is true)
+ @if (Model.Features?.Any() is true)
{
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
@@ -76,16 +76,16 @@
</tr>
</thead>
<tbody>
- @for (int i = 0; i < Model.Entitlements.Count; i++)
+ @for (int i = 0; i < Model.Features.Count; i++)
{
<tr>
<td>
- <input asp-for="Entitlements[i].Id" class="form-control">
- <span asp-validation-for="Entitlements[i].Id" class="text-danger"></span>
+ <input asp-for="Features[i].Id" class="form-control">
+ <span asp-validation-for="Features[i].Id" class="text-danger"></span>
</td>
<td>
- <input asp-for="Entitlements[i].ShortDescription" class="form-control w-100">
- <span asp-validation-for="Entitlements[i].ShortDescription" class="text-danger"></span>
+ <input asp-for="Features[i].ShortDescription" class="form-control w-100">
+ <span asp-validation-for="Features[i].ShortDescription" class="text-danger"></span>
</td>
<td>
<button type="submit" name="removeIndex" value="@i" class="d-inline-block btn text-danger btn-link">
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOfferingViewModel.cs b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOfferingViewModel.cs
index 3c1db0f..67db7d3 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOfferingViewModel.cs
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIOffering/ConfigureOfferingViewModel.cs
@@ -18,19 +18,19 @@ public class ConfigureOfferingViewModel
Name = offeringData.App.Name;
OriginalName = Name;
SuccessRedirectUrl = offeringData.SuccessRedirectUrl;
- foreach (var entitlement in offeringData.Entitlements.OrderBy(b => b.CustomId))
+ foreach (var feature in offeringData.Features.OrderBy(b => b.CustomId))
{
- Entitlements.Add(new EntitlementViewModel()
+ Features.Add(new FeatureViewModel()
{
- Id = entitlement.CustomId,
- ShortDescription = entitlement.Description
+ Id = feature.CustomId,
+ ShortDescription = feature.Description
});
}
Data = offeringData;
}
public OfferingData Data { get; set; }
- public class EntitlementViewModel
+ public class FeatureViewModel
{
[StringLength(50)]
[Required]
@@ -49,6 +49,6 @@ public class ConfigureOfferingViewModel
[Display(Name = "Success redirect url")]
public string SuccessRedirectUrl { get; set; }
- public List<EntitlementViewModel> Entitlements { get; set; } = new();
+ public List<FeatureViewModel> Features { get; set; } = new();
public string Anchor { get; set; }
}
diff --git a/BTCPayServer/Plugins/Subscriptions/Views/UIPlanCheckout/PlanCheckout.cshtml b/BTCPayServer/Plugins/Subscriptions/Views/UIPlanCheckout/PlanCheckout.cshtml
index 455e8f5..2464de4 100644
--- a/BTCPayServer/Plugins/Subscriptions/Views/UIPlanCheckout/PlanCheckout.cshtml
+++ b/BTCPayServer/Plugins/Subscriptions/Views/UIPlanCheckout/PlanCheckout.cshtml
@@ -194,15 +194,15 @@
<div class="price-period">per month</div>
</div>
- @if (Model.Data.PlanEntitlements.Count(i => !string.IsNullOrWhiteSpace(i.Entitlement.Description)) != 0)
+ @if (Model.Data.PlanFeatures.Count(i => !string.IsNullOrWhiteSpace(i.Feature.Description)) != 0)
{
<h5 class="mb-3">What's included:</h5>
<ul class="feature-list">
- @foreach (var item in Model.Data.PlanEntitlements.Where(i => !string.IsNullOrWhiteSpace(i.Entitlement.Description)))
+ @foreach (var item in Model.Data.PlanFeatures.Where(i => !string.IsNullOrWhiteSpace(i.Feature.Description)))
{
<li class="feature-item">
<i class="fas fa fa-check feature-icon"></i>
- <span>@item.Entitlement.Description</span>
+ <span>@item.Feature.Description</span>
</li>
}
</ul>
diff --git a/BTCPayServer/Services/Translations.Default.cs b/BTCPayServer/Services/Translations.Default.cs
index aef2543..ec29b86 100644
--- a/BTCPayServer/Services/Translations.Default.cs
+++ b/BTCPayServer/Services/Translations.Default.cs
@@ -617,7 +617,7 @@ namespace BTCPayServer.Services
"Enter the wallet seed": "",
"Enter wallet seed": "",
"Enter your extended public key": "",
- "Entitlements": "",
+ "Features": "",
"Error": "",
"Error updating profile": "",
"Error updating user": "",
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.