using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Tiku.Application.Security; using Tiku.Domain.Platform; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Caching; using Tiku.Infrastructure.Persistence; using ZiggyCreatures.Caching.Fusion; namespace Tiku.Infrastructure.Security; internal sealed record TenantFeatureDefinition(string Code, bool IsCore); internal sealed record TenantSubscriptionSnapshot( Guid Id, Guid BaseOfferingVersionId, TenantSaasSubscriptionStatus Status, DateTimeOffset StartsAt, DateTimeOffset CurrentPeriodEnd); internal sealed record TenantFeatureAccessSnapshot( TenantStatus? TenantStatus, TenantSubscriptionSnapshot? Subscription, TenantFeatureDefinition[] Features, Dictionary Overrides, HashSet PurchasedFeatures) { public FeatureAccessDecision Evaluate(string featureCode, FeatureAccessOperation operation) { var normalized = Normalize(featureCode); if (TenantStatus != Domain.Tenancy.TenantStatus.Active) return Denied(normalized, operation, "tenant_inactive"); var feature = Features.SingleOrDefault(value => value.Code == normalized); if (feature is null) return Denied(normalized, operation, "feature_unavailable"); if (feature.IsCore) return Allowed(normalized, operation); var hasOverride = Overrides.TryGetValue(normalized, out var overrideMode); if (hasOverride && overrideMode == TenantFeatureOverrideMode.Disabled) return Denied(normalized, operation, "feature_disabled"); if (Subscription is null) return hasOverride && overrideMode == TenantFeatureOverrideMode.Enabled ? Allowed(normalized, operation) : Denied(normalized, operation, "subscription_missing"); var now = DateTimeOffset.UtcNow; if (operation == FeatureAccessOperation.Write && (Subscription.Status is not (TenantSaasSubscriptionStatus.Trial or TenantSaasSubscriptionStatus.Active) || Subscription.StartsAt > now || Subscription.CurrentPeriodEnd <= now)) return Denied(normalized, operation, "subscription_read_only"); if (Subscription.Status == TenantSaasSubscriptionStatus.Suspended) return Denied(normalized, operation, "subscription_suspended"); if (hasOverride && overrideMode == TenantFeatureOverrideMode.Enabled) return Allowed(normalized, operation); return PurchasedFeatures.Contains(normalized) ? Allowed(normalized, operation) : Denied(normalized, operation, "feature_not_purchased"); } private static string Normalize(string value) { return value.Trim().ToLowerInvariant(); } private static FeatureAccessDecision Allowed(string featureCode, FeatureAccessOperation operation) { return new FeatureAccessDecision(true, null, featureCode, operation); } private static FeatureAccessDecision Denied( string featureCode, FeatureAccessOperation operation, string denialCode) { return new FeatureAccessDecision(false, denialCode, featureCode, operation); } } internal interface ITenantFeatureSnapshotProvider { Task GetAsync( Guid tenantId, FeatureAccessOperation operation, CancellationToken cancellationToken = default); } internal sealed class TenantFeatureSnapshotProvider( IPlatformControlPlanePersistence platformControlPlanePersistence, ITenancyPersistence tenancyPersistence, [FromKeyedServices(BusinessCachingServiceCollectionExtensions.CacheName)] IFusionCache cache) : ITenantFeatureSnapshotProvider { private readonly Dictionary<(Guid TenantId, FeatureAccessOperation Operation), (long SaveVersion, Task Snapshot)> requestCache = []; public Task GetAsync( Guid tenantId, FeatureAccessOperation operation, CancellationToken cancellationToken = default) { var requestKey = (tenantId, operation); var saveVersion = platformControlPlanePersistence.SaveVersion; if (!requestCache.TryGetValue(requestKey, out var cached) || cached.SaveVersion != saveVersion) { var snapshotTask = GetCoreAsync( tenantId, operation, cached.Snapshot is not null, cancellationToken); requestCache[requestKey] = (saveVersion, snapshotTask); return snapshotTask; } return cached.Snapshot; } private async Task GetCoreAsync( Guid tenantId, FeatureAccessOperation operation, bool bypassSharedCache, CancellationToken cancellationToken) { var cacheKey = CacheKey(tenantId, operation); return await cache.GetOrSetAsync( cacheKey, (_, token) => LoadAsync(tenantId, operation, token), options => { options.Duration = TimeSpan.FromSeconds(60); options.MemoryCacheDuration = TimeSpan.FromSeconds(30); options.SkipMemoryCacheRead = bypassSharedCache; options.SkipDistributedCacheRead = bypassSharedCache; }, token: cancellationToken); } private async Task LoadAsync( Guid tenantId, FeatureAccessOperation operation, CancellationToken cancellationToken) { var tenant = await tenancyPersistence.Tenants.AsNoTracking() .Where(value => value.Id == tenantId) .Select(value => new { Status = (TenantStatus?)value.Status, Subscription = platformControlPlanePersistence.TenantSaasSubscriptions.AsNoTracking() .Where(subscription => subscription.TenantId == tenantId) .OrderByDescending(subscription => subscription.UpdatedAt) .Select(subscription => new TenantSubscriptionSnapshot( subscription.Id, subscription.BaseOfferingVersionId, subscription.Status, subscription.StartsAt, subscription.CurrentPeriodEnd)) .FirstOrDefault() }) .SingleOrDefaultAsync(cancellationToken); var features = await platformControlPlanePersistence.SaasFeatures.AsNoTracking() .Where(value => value.Status == SaasFeatureStatus.Active) .Select(value => new TenantFeatureDefinition(value.Code, value.IsCore)) .ToArrayAsync(cancellationToken); var now = DateTimeOffset.UtcNow; var overrides = await platformControlPlanePersistence.TenantFeatureOverrides.AsNoTracking() .Where(value => value.TenantId == tenantId && (value.ExpiresAt == null || value.ExpiresAt > now)) .ToDictionaryAsync(value => value.FeatureCode, value => value.Mode, StringComparer.Ordinal, cancellationToken); var purchased = new HashSet(StringComparer.Ordinal); if (tenant?.Subscription is { } subscription) { var eligibleVersionIds = platformControlPlanePersistence.TenantSaasSubscriptionItems.AsNoTracking() .Where(value => value.TenantId == tenantId && value.SubscriptionId == subscription.Id && (operation == FeatureAccessOperation.Read ? value.Status != TenantSaasSubscriptionItemStatus.Pending && value.Status != TenantSaasSubscriptionItemStatus.Scheduled && value.StartsAt <= now : value.Status == TenantSaasSubscriptionItemStatus.Active && value.StartsAt <= now && value.EndsAt > now)) .Select(value => value.OfferingVersionId) .Concat(platformControlPlanePersistence.TenantSaasSubscriptions.AsNoTracking() .Where(value => value.Id == subscription.Id) .Select(value => value.BaseOfferingVersionId)); purchased = (await platformControlPlanePersistence.SaasOfferingVersionFeatures.AsNoTracking() .Where(value => eligibleVersionIds.Contains(value.OfferingVersionId)) .Select(value => value.FeatureCode) .Distinct() .ToArrayAsync(cancellationToken)) .ToHashSet(StringComparer.Ordinal); } return new TenantFeatureAccessSnapshot( tenant?.Status, tenant?.Subscription, features, overrides, purchased); } internal static string CacheKey(Guid tenantId, FeatureAccessOperation operation) { return $"tenant-feature-snapshot:v1:{tenantId:N}:{operation.ToString().ToLowerInvariant()}"; } }