Open Closed

The SaaS module features will not become invalid after the edition expires. #10004


User avatar
0
464199480 created

I think the feature should become invalid upon expiration.


4 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In the ABP SaaS module, features assigned via an edition are linked to the tenant's subscription. When an edition (and its subscription) expires, the expected behavior is that the features provided by that edition should no longer be available to the tenant.

    • However, by default, the ABP SaaS module does not automatically invalidate or remove the features from the tenant when the edition expires. The features remain accessible unless you implement custom logic to handle feature invalidation upon edition expiration.

    • To achieve the behavior where features become invalid after the edition expires, you need to implement a mechanism (such as a background job or event handler) that checks for expired subscriptions and updates or removes the tenant's feature values accordingly.

    • The ABP SaaS module provides extensibility points where you can hook into subscription/edition changes and enforce your own business rules for feature availability.

    • For more details, refer to the official documentation and related support discussions:

    Sources:


    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

  • User Avatar
    0
    464199480 created

    Yes, the subscription has expired.

  • User Avatar
    0
    464199480 created

    If the feature cannot fail, what is the purpose of the expiration?

  • User Avatar
    0
    enisn created
    Support Team .NET Developer

    Issue Summary

    Thank you for reporting this issue. We have investigated and confirmed that edition features continue to work even after the edition subscription expires. This is indeed a bug in the ABP Framework's feature management system.

    What's Happening?

    When you set an edition with an expiration date for a tenant:

    • The EditionEndDateUtc is correctly stored in the database
    • The edition expiration date is tracked properly
    • However, features associated with that edition remain accessible after the expiration date

    This affects subscription-based applications where features should be disabled when a subscription expires.


    Root Cause

    The issue is in the ABP Framework's EditionFeatureValueProvider class, which is responsible for retrieving feature values for tenants based on their edition. Currently, this provider:

    1. ✅ Correctly retrieves the tenant's EditionId
    2. Does NOT check if the edition has expired (EditionEndDateUtc)
    3. ❌ Returns features from expired editions

    The SaaS module has the necessary data (EditionEndDateUtc) and logic (GetActiveEditionId() method) to handle expiration, but the feature system doesn't use this information.


    Impact

    Affected Scenarios

    • Subscription Expiration: Tenants can continue using premium features after their subscription expires
    • Trial Periods: Trial editions with time limits don't properly restrict access after expiration
    • Payment Integration: When payment subscriptions expire or are canceled, features remain active
    • Manual Edition Management: Setting an expiration date doesn't automatically disable features

    What Still Works Correctly

    • ✅ Edition claims in user tokens are correctly set (expired editions don't appear in claims)
    • ✅ Payment subscription events properly update the expiration dates
    • ✅ Tenant activation states work as expected
    • ✅ Database queries for expired editions work correctly

    Immediate Workarounds

    While we work on a permanent fix, here are some workarounds you can implement a workaround.

    Workaround : Handle Subscription Events

    When a subscription is canceled or updated, you can handle these events:

    using Volo.Abp.DependencyInjection;
    using Volo.Abp.EventBus.Distributed;
    using Volo.Payment.Subscription;
    
    public class CustomSubscriptionEventHandler : 
        IDistributedEventHandler<SubscriptionCanceledEto>,
        IDistributedEventHandler<SubscriptionUpdatedEto>,
        ITransientDependency
    {
        private readonly IDistributedCache<TenantConfigurationCacheItem> _cache;
        private readonly ILogger<CustomSubscriptionEventHandler> _logger;
        private readonly IEmailSender _emailSender;
    
        public CustomSubscriptionEventHandler(
            IDistributedCache<TenantConfigurationCacheItem> cache,
            ILogger<CustomSubscriptionEventHandler> logger,
            IEmailSender emailSender)
        {
            _cache = cache;
            _logger = logger;
            _emailSender = emailSender;
        }
    
        public async Task HandleEventAsync(SubscriptionCanceledEto eventData)
        {
            var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString());
            
            _logger.LogWarning($"Subscription canceled for tenant {tenantId}");
    
            // *** Set the EditionId of the tenant to null if needed: ***
    
            // var tenant = await TenantRepository.FindAsync(tenantId, includeDetails: false);
            // tenant.EditionId = null;
            // await TenantRepository.UpdateAsync(tenant);        
    
            // Clear tenant configuration cache to force feature re-check
            await InvalidateTenantCacheAsync(tenantId);
            
            // Send notification email
            await _emailSender.SendAsync(
                to: await GetTenantAdminEmailAsync(tenantId),
                subject: "Subscription Canceled",
                body: "Your subscription has been canceled. You have access until the end of your billing period."
            );
        }
    
        public async Task HandleEventAsync(SubscriptionUpdatedEto eventData)
        {
            var tenantId = Guid.Parse(eventData.ExtraProperties[TenantConsts.TenantIdParameterName]?.ToString());
            
            // Check if subscription expired (PeriodEndDate is in the past)
            if (eventData.PeriodEndDate.HasValue && eventData.PeriodEndDate < DateTime.UtcNow)
            {
                _logger.LogWarning($"Subscription expired for tenant {tenantId}");
                
    
                // *** Set the EditionId of the tenant to null if needed: ***
    
                // var tenant = await TenantRepository.FindAsync(tenantId, includeDetails: false);
                // tenant.EditionId = null;
                // await TenantRepository.UpdateAsync(tenant);
    
                // Clear cache to force feature re-check
                await InvalidateTenantCacheAsync(tenantId);
    
                // Send expiration notification
                await _emailSender.SendAsync(
                    to: await GetTenantAdminEmailAsync(tenantId),
                    subject: "Subscription Expired",
                    body: "Your subscription has expired. Please renew to continue using premium features."
                );
            }
        }
    
        private async Task InvalidateTenantCacheAsync(Guid tenantId)
        {
            // Clear tenant configuration cache
            await _cache.RemoveAsync(
                TenantConfigurationCacheItem.CalculateCacheKey(tenantId, null),
                considerUow: true
            );
        }
    
        private async Task<string> GetTenantAdminEmailAsync(Guid tenantId)
        {
            // Implement logic to get tenant admin email
            return "admin@tenant.com";
        }
    }
    

    Testing Your Workaround

    To verify your workaround is working:

    Test Case 1: Expired Edition

    1. Create a test tenant with an edition that has features
    2. Set the tenant's EditionEndDateUtc to a past date:
      var tenant = await TenantRepository.GetAsync(tenantId);
      tenant.EditionEndDateUtc = DateTime.UtcNow.AddDays(-1);
      await TenantRepository.UpdateAsync(tenant);
      
    3. Try to access a feature from that edition
    4. Expected: Access should be denied
    5. Without fix: Access is still granted

    Test Case 2: Active Edition

    1. Use a tenant with an edition expiring in the future
    2. Access features from that edition
    3. Expected: Access should be granted

    Test Case 3: Edition Renewal

    1. Start with a tenant with an expired edition (blocked access)
    2. Renew the subscription:
      var tenant = await TenantRepository.GetAsync(tenantId);
      tenant.EditionEndDateUtc = DateTime.UtcNow.AddMonths(1);
      await TenantRepository.UpdateAsync(tenant);
      
    3. Access features again
    4. Expected: Access should now be granted

    Migration Guide for Existing Data

    If you have existing tenants with expired editions that should be blocked:

    Step 1: Identify Affected Tenants

    var expiredTenants = await TenantRepository.GetListAsync(
        expirationDateMax: DateTime.UtcNow
    );
    
    foreach (var tenant in expiredTenants)
    {
        if (tenant.EditionId != null && tenant.GetActiveEditionId() == null)
        {
            Logger.LogInformation($"Tenant {tenant.Name} has expired edition {tenant.EditionId}");
        }
    }
    

    Step 2: Decide on Action

    For each expired tenant, you can:

    Option A: Clear Edition (if they shouldn't have any edition features)

    tenant.EditionId = null;
    tenant.EditionEndDateUtc = null;
    await TenantRepository.UpdateAsync(tenant);
    

    Option B: Set to Free/Trial Edition

    var freeEditionId = await GetFreeEditionIdAsync();
    tenant.EditionId = freeEditionId;
    tenant.EditionEndDateUtc = null; // Perpetual free edition
    await TenantRepository.UpdateAsync(tenant);
    

    Option C: Extend Expiration (give them grace period)

    tenant.EditionEndDateUtc = DateTime.UtcNow.AddDays(7); // 7 day grace
    await TenantRepository.UpdateAsync(tenant);
    

    Step 3: Notify Affected Users

    foreach (var tenant in expiredTenants)
    {
        await EmailService.SendAsync(
            to: await GetTenantAdminEmailAsync(tenant.Id),
            subject: "Subscription Expired",
            body: $"Your subscription has expired. Please renew to continue using premium features."
        );
    }
    

    Updates and Progress

    We'll update this document as we make progress on the fix. You can track:

    • This support thread for updates
    • ABP GitHub repository for code changes
    • Release notes for the version containing the fix

    We understand this impacts subscription-based applications, and we're prioritizing a fix. In the meantime, please implement one of the workarounds provided above.

    If you need further assistance implementing any of these workarounds, please don't hesitate to reach out.

Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.1.0-preview. Updated on October 27, 2025, 08:34