Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Host/FSH.Starter.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"Billing": {
"DefaultPlanKey": "free",
"GraceWindowDays": 7
"GracePeriodDays": 7
},
"OpenTelemetryOptions": {
"Enabled": true,
Expand Down
2 changes: 1 addition & 1 deletion src/Modules/Identity/Modules.Identity/IdentityModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void ConfigureServices(IHostApplicationBuilder builder)
// Configure password policy options
services.Configure<PasswordPolicyOptions>(builder.Configuration.GetSection("PasswordPolicy"));

// Tenant subscription grace window (shared "Billing" section) — used by the login expiry check.
// Tenant subscription grace period (shared "Billing" section) — used by the login expiry check.
services.Configure<TenantGraceOptions>(builder.Configuration.GetSection(TenantGraceOptions.SectionName));

// Register password history service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public sealed class IdentityService : IIdentityService
private readonly IGroupRoleService _groupRoleService;
private readonly TimeProvider _timeProvider;
private readonly IdentityDbContext _dbContext;
private readonly int _graceWindowDays;
private readonly int _gracePeriodDays;

public IdentityService(
UserManager<FshUser> userManager,
Expand All @@ -41,7 +41,7 @@ public IdentityService(
_groupRoleService = groupRoleService;
_timeProvider = timeProvider;
_dbContext = dbContext;
_graceWindowDays = graceOptions.Value.GraceWindowDays;
_gracePeriodDays = graceOptions.Value.GracePeriodDays;
}

public async Task<(string Subject, IEnumerable<Claim> Claims)?>
Expand Down Expand Up @@ -286,9 +286,9 @@ private void ValidateTenantStatus(AppTenantInfo tenant)
throw new UnauthorizedException($"tenant {tenant.Id} is deactivated");
}

// Honor the billing grace window: a lapsed tenant can still authenticate until
// Honor the billing grace period: a lapsed tenant can still authenticate until
// ValidUpto + grace (matching the request-time guard in MultitenancyModule).
if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_graceWindowDays))
if (_timeProvider.GetUtcNow().UtcDateTime > tenant.ValidUpto.AddDays(_gracePeriodDays))
{
throw new UnauthorizedException($"tenant {tenant.Id} validity has expired");
}
Expand Down
6 changes: 3 additions & 3 deletions src/Modules/Identity/Modules.Identity/TenantGraceOptions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
namespace FSH.Modules.Identity;

/// <summary>
/// Login-side view of the tenant billing grace window (config section <c>"Billing"</c>). A tenant
/// whose subscription has lapsed can still authenticate until <c>ValidUpto + GraceWindowDays</c>.
/// Login-side view of the tenant billing grace period (config section <c>"Billing"</c>). A tenant
/// whose subscription has lapsed can still authenticate until <c>ValidUpto + GracePeriodDays</c>.
/// </summary>
public sealed class TenantGraceOptions
{
public const string SectionName = "Billing";

public int GraceWindowDays { get; set; } = 7;
public int GracePeriodDays { get; set; } = 7;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ public sealed class TenantStatusDto
/// <summary>Derived lifecycle state: "Active", "InGrace", or "Expired".</summary>
public string ExpiryState { get; init; } = "Active";

/// <summary>Instant after which a lapsed tenant is hard-blocked (ValidUpto + grace window).</summary>
/// <summary>Instant after which a lapsed tenant is hard-blocked (ValidUpto + grace period).</summary>
public DateTime GraceEndsUtc { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace FSH.Modules.Multitenancy.Contracts.Events;

/// <summary>
/// Raised by the daily expiry scan when a tenant has passed <c>ValidUpto</c> but is still inside the
/// grace window (access continues). Consumers warn the tenant that the grace period is counting down.
/// grace period (access continues). Consumers warn the tenant that the grace period is counting down.
/// </summary>
public sealed record TenantEnteredGraceIntegrationEvent(
Guid Id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,18 @@ public void ConfigureMiddleware(IApplicationBuilder app)
throw new ForbiddenException("This tenant has been deactivated. Contact your administrator.");
}

// Expiry is enforced on every request (not just at login) with a grace window:
// Expiry is enforced on every request (not just at login) with a grace period:
// a tenant past ValidUpto still works until ValidUpto + grace, then is hard-blocked.
var graceDays = ctx.RequestServices
.GetRequiredService<IOptions<TenantBillingOptions>>().Value.GraceWindowDays;
.GetRequiredService<IOptions<TenantBillingOptions>>().Value.GracePeriodDays;
var nowUtc = ctx.RequestServices.GetRequiredService<TimeProvider>().GetUtcNow().UtcDateTime;
var graceEndsUtc = tenant.ValidUpto.AddDays(graceDays);
if (nowUtc > graceEndsUtc)
{
throw new ForbiddenException("This tenant's subscription has expired. Please renew to continue.");
}

// Inside the grace window: surface days-left so clients can warn. Set via OnStarting so
// Inside the grace period: surface days-left so clients can warn. Set via OnStarting so
// the header survives even when an exception handler rewrites the response.
if (nowUtc > tenant.ValidUpto)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public async Task RunAsync(CancellationToken cancellationToken)
private async Task<bool> TryNotifyAsync(AppTenantInfo tenant, DateTime now, CancellationToken ct)
{
var validUpto = tenant.ValidUpto;
var graceEnds = validUpto.AddDays(_options.GraceWindowDays);
var graceEnds = validUpto.AddDays(_options.GracePeriodDays);

string noticeType;
if (now > graceEnds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ public async Task<TenantStatusDto> GetStatusAsync(string id, CancellationToken c
{
var tenant = await GetTenantInfoAsync(id, cancellationToken).ConfigureAwait(false);

var graceEnds = tenant.ValidUpto.AddDays(_billingOptions.GraceWindowDays);
var graceEnds = tenant.ValidUpto.AddDays(_billingOptions.GracePeriodDays);
var now = _timeProvider.GetUtcNow().UtcDateTime;
string expiryState;
if (now <= tenant.ValidUpto)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public sealed class TenantBillingOptions
public string DefaultPlanKey { get; set; } = "free";

/// <summary>Days past <c>ValidUpto</c> during which requests/logins still succeed.</summary>
public int GraceWindowDays { get; set; } = 7;
public int GracePeriodDays { get; set; } = 7;

/// <summary>How many days before <c>ValidUpto</c> the daily scan starts sending "nearing expiry" reminders.</summary>
public int ExpiryNotificationLeadDays { get; set; } = 7;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public async Task AdjustValidity_Should_Allow_Backdating_ToExpireImmediately()
var planKey = await CreatePlanAsync(rootClient, $"adj-b-{unique}", monthlyBasePrice: 10m);
await CreateTenantAsync(rootClient, tenantId, $"adj-back-{unique}@tenant.com", planKey);

// Backdate well past the grace window — renewal would reject this; the override allows it.
// Backdate well past the grace period — renewal would reject this; the override allows it.
var target = DateTime.UtcNow.AddDays(-30);
var response = await rootClient.PostAsJsonAsync(
$"{TestConstants.TenantsBasePath}/{tenantId}/adjust-validity",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Integration.Tests.Tests.Multitenancy;

/// <summary>
/// Verifies subscription-expiry enforcement in the post-auth tenant guard: a tenant past
/// <c>ValidUpto</c> but within the grace window still passes (so requests/logins keep working during
/// <c>ValidUpto</c> but within the grace period still passes (so requests/logins keep working during
/// dunning), while a tenant past <c>ValidUpto + grace</c> is hard-blocked with 403 — mirroring the
/// deactivation guard. Grace defaults to 7 days.
/// </summary>
Expand All @@ -33,7 +33,7 @@ public async Task ExpiredTenant_PastGrace_Should_Be_Blocked()
// While valid, the guard does not short-circuit — the token handler runs and fails on creds.
(await TryIssueTokenAsync(tenantId)).ShouldNotBe(HttpStatusCode.Forbidden);

// Lapse well past the 7-day grace window.
// Lapse well past the 7-day grace period.
await SetTenantValidityAsync(tenantId, DateTime.UtcNow.AddDays(-30));

(await TryIssueTokenAsync(tenantId)).ShouldBe(HttpStatusCode.Forbidden,
Expand All @@ -45,25 +45,25 @@ public async Task LapsedTenant_WithinGrace_Should_Not_Be_Blocked()
{
var tenantId = await CreateTenantAsync();

// One day past expiry is still inside the 7-day grace window.
// One day past expiry is still inside the 7-day grace period.
await SetTenantValidityAsync(tenantId, DateTime.UtcNow.AddDays(-1));

(await TryIssueTokenAsync(tenantId)).ShouldNotBe(HttpStatusCode.Forbidden,
"a lapsed tenant within the grace window must still be allowed through the guard");
"a lapsed tenant within the grace period must still be allowed through the guard");
}

[Fact]
public async Task LapsedTenant_WithinGrace_Should_Emit_GraceHeader()
{
var tenantId = await CreateTenantAsync();

// One day past expiry — inside the 7-day grace window.
// One day past expiry — inside the 7-day grace period.
await SetTenantValidityAsync(tenantId, DateTime.UtcNow.AddDays(-1));

var (status, grace) = await ProbeAsync(tenantId);

status.ShouldNotBe(HttpStatusCode.Forbidden);
grace.ShouldNotBeNull("a tenant in the grace window must receive the X-Subscription-Grace header");
grace.ShouldNotBeNull("a tenant in the grace period must receive the X-Subscription-Grace header");
int.Parse(grace!, CultureInfo.InvariantCulture).ShouldBeInRange(1, 7);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public async Task ScanJob_Should_Record_GraceNotice_Dedup_And_Email()
var planKey = await CreatePlanAsync(rootClient, $"scan-m-{unique}", 10m);
await CreateTenantAsync(rootClient, tenantId, adminEmail, planKey);

// Lapse into the grace window (1 day past ValidUpto).
// Lapse into the grace period (1 day past ValidUpto).
var adjust = await rootClient.PostAsJsonAsync(
$"{TestConstants.TenantsBasePath}/{tenantId}/adjust-validity",
new { tenantId, validUpto = DateTime.UtcNow.AddDays(-1) });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Multitenancy.Tests.Services;
/// <summary>
/// Pins the expiry-state transitions in <see cref="TenantService.GetStatusAsync"/> exactly at the
/// ValidUpto and grace-window boundaries, so the Active → InGrace → Expired badges never drift.
/// Grace window is fixed at 7 days for these cases.
/// Grace period is fixed at 7 days for these cases.
/// </summary>
public sealed class TenantServiceStatusBoundaryTests
{
Expand Down Expand Up @@ -47,7 +47,7 @@ public async Task GetStatusAsync_Should_ResolveExpiryState_AtBoundary(double off
dbContext: null!,
provisioningService: null!,
_clock,
Options.Create(new TenantBillingOptions { GraceWindowDays = GraceDays }),
Options.Create(new TenantBillingOptions { GracePeriodDays = GraceDays }),
NullLogger<TenantService>.Instance);

var status = await sut.GetStatusAsync(tenantId, CancellationToken.None);
Expand Down
Loading