1
0
mirror of https://github.com/bitwarden/server synced 2025-12-24 04:03:25 +00:00

[PM-26194] Fix: Provider Portal not automatically disabled, when subscription is cancelled (#6480)

* Add the fix for the bug

* Move the org disable to job
This commit is contained in:
cyprain-okeke
2025-10-27 13:19:42 +01:00
committed by GitHub
parent 9b313d9c0a
commit 427600d0cc
4 changed files with 500 additions and 2 deletions

View File

@@ -0,0 +1,88 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Quartz;
namespace Bit.Billing.Jobs;
public class ProviderOrganizationDisableJob(
IProviderOrganizationRepository providerOrganizationRepository,
IOrganizationDisableCommand organizationDisableCommand,
ILogger<ProviderOrganizationDisableJob> logger)
: IJob
{
private const int MaxConcurrency = 5;
private const int MaxTimeoutMinutes = 10;
public async Task Execute(IJobExecutionContext context)
{
var providerId = new Guid(context.MergedJobDataMap.GetString("providerId") ?? string.Empty);
var expirationDateString = context.MergedJobDataMap.GetString("expirationDate");
DateTime? expirationDate = string.IsNullOrEmpty(expirationDateString)
? null
: DateTime.Parse(expirationDateString);
logger.LogInformation("Starting to disable organizations for provider {ProviderId}", providerId);
var startTime = DateTime.UtcNow;
var totalProcessed = 0;
var totalErrors = 0;
try
{
var providerOrganizations = await providerOrganizationRepository
.GetManyDetailsByProviderAsync(providerId);
if (providerOrganizations == null || !providerOrganizations.Any())
{
logger.LogInformation("No organizations found for provider {ProviderId}", providerId);
return;
}
logger.LogInformation("Disabling {OrganizationCount} organizations for provider {ProviderId}",
providerOrganizations.Count, providerId);
var semaphore = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);
var tasks = providerOrganizations.Select(async po =>
{
if (DateTime.UtcNow.Subtract(startTime).TotalMinutes > MaxTimeoutMinutes)
{
logger.LogWarning("Timeout reached while disabling organizations for provider {ProviderId}", providerId);
return false;
}
await semaphore.WaitAsync();
try
{
await organizationDisableCommand.DisableAsync(po.OrganizationId, expirationDate);
Interlocked.Increment(ref totalProcessed);
return true;
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to disable organization {OrganizationId} for provider {ProviderId}",
po.OrganizationId, providerId);
Interlocked.Increment(ref totalErrors);
return false;
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
logger.LogInformation("Completed disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
providerId, totalProcessed, totalErrors);
}
catch (Exception ex)
{
logger.LogError(ex, "Error disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
providerId, totalProcessed, totalErrors);
throw;
}
}
}

View File

@@ -1,7 +1,11 @@
using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Services;
using Quartz;
using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
@@ -11,17 +15,26 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
private readonly IUserService _userService;
private readonly IStripeEventUtilityService _stripeEventUtilityService;
private readonly IOrganizationDisableCommand _organizationDisableCommand;
private readonly IProviderRepository _providerRepository;
private readonly IProviderService _providerService;
private readonly ISchedulerFactory _schedulerFactory;
public SubscriptionDeletedHandler(
IStripeEventService stripeEventService,
IUserService userService,
IStripeEventUtilityService stripeEventUtilityService,
IOrganizationDisableCommand organizationDisableCommand)
IOrganizationDisableCommand organizationDisableCommand,
IProviderRepository providerRepository,
IProviderService providerService,
ISchedulerFactory schedulerFactory)
{
_stripeEventService = stripeEventService;
_userService = userService;
_stripeEventUtilityService = stripeEventUtilityService;
_organizationDisableCommand = organizationDisableCommand;
_providerRepository = providerRepository;
_providerService = providerService;
_schedulerFactory = schedulerFactory;
}
/// <summary>
@@ -53,9 +66,38 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());
}
else if (providerId.HasValue)
{
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
if (provider != null)
{
provider.Enabled = false;
await _providerService.UpdateAsync(provider);
await QueueProviderOrganizationDisableJobAsync(providerId.Value, subscription.GetCurrentPeriodEnd());
}
}
else if (userId.HasValue)
{
await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
}
}
private async Task QueueProviderOrganizationDisableJobAsync(Guid providerId, DateTime? expirationDate)
{
var scheduler = await _schedulerFactory.GetScheduler();
var job = JobBuilder.Create<ProviderOrganizationDisableJob>()
.WithIdentity($"disable-provider-orgs-{providerId}", "provider-management")
.UsingJobData("providerId", providerId.ToString())
.UsingJobData("expirationDate", expirationDate?.ToString("O"))
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"disable-trigger-{providerId}", "provider-management")
.StartNow()
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}