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:
88
src/Billing/Jobs/ProviderOrganizationDisableJob.cs
Normal file
88
src/Billing/Jobs/ProviderOrganizationDisableJob.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user