Skip to content

Commit 427600d

Browse files
[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
1 parent 9b313d9 commit 427600d

File tree

4 files changed

+500
-2
lines changed

4 files changed

+500
-2
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// FIXME: Update this file to be null safe and then delete the line below
2+
#nullable disable
3+
4+
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
5+
using Bit.Core.AdminConsole.Repositories;
6+
using Quartz;
7+
8+
namespace Bit.Billing.Jobs;
9+
10+
public class ProviderOrganizationDisableJob(
11+
IProviderOrganizationRepository providerOrganizationRepository,
12+
IOrganizationDisableCommand organizationDisableCommand,
13+
ILogger<ProviderOrganizationDisableJob> logger)
14+
: IJob
15+
{
16+
private const int MaxConcurrency = 5;
17+
private const int MaxTimeoutMinutes = 10;
18+
19+
public async Task Execute(IJobExecutionContext context)
20+
{
21+
var providerId = new Guid(context.MergedJobDataMap.GetString("providerId") ?? string.Empty);
22+
var expirationDateString = context.MergedJobDataMap.GetString("expirationDate");
23+
DateTime? expirationDate = string.IsNullOrEmpty(expirationDateString)
24+
? null
25+
: DateTime.Parse(expirationDateString);
26+
27+
logger.LogInformation("Starting to disable organizations for provider {ProviderId}", providerId);
28+
29+
var startTime = DateTime.UtcNow;
30+
var totalProcessed = 0;
31+
var totalErrors = 0;
32+
33+
try
34+
{
35+
var providerOrganizations = await providerOrganizationRepository
36+
.GetManyDetailsByProviderAsync(providerId);
37+
38+
if (providerOrganizations == null || !providerOrganizations.Any())
39+
{
40+
logger.LogInformation("No organizations found for provider {ProviderId}", providerId);
41+
return;
42+
}
43+
44+
logger.LogInformation("Disabling {OrganizationCount} organizations for provider {ProviderId}",
45+
providerOrganizations.Count, providerId);
46+
47+
var semaphore = new SemaphoreSlim(MaxConcurrency, MaxConcurrency);
48+
var tasks = providerOrganizations.Select(async po =>
49+
{
50+
if (DateTime.UtcNow.Subtract(startTime).TotalMinutes > MaxTimeoutMinutes)
51+
{
52+
logger.LogWarning("Timeout reached while disabling organizations for provider {ProviderId}", providerId);
53+
return false;
54+
}
55+
56+
await semaphore.WaitAsync();
57+
try
58+
{
59+
await organizationDisableCommand.DisableAsync(po.OrganizationId, expirationDate);
60+
Interlocked.Increment(ref totalProcessed);
61+
return true;
62+
}
63+
catch (Exception ex)
64+
{
65+
logger.LogError(ex, "Failed to disable organization {OrganizationId} for provider {ProviderId}",
66+
po.OrganizationId, providerId);
67+
Interlocked.Increment(ref totalErrors);
68+
return false;
69+
}
70+
finally
71+
{
72+
semaphore.Release();
73+
}
74+
});
75+
76+
await Task.WhenAll(tasks);
77+
78+
logger.LogInformation("Completed disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
79+
providerId, totalProcessed, totalErrors);
80+
}
81+
catch (Exception ex)
82+
{
83+
logger.LogError(ex, "Error disabling organizations for provider {ProviderId}. Processed: {TotalProcessed}, Errors: {TotalErrors}",
84+
providerId, totalProcessed, totalErrors);
85+
throw;
86+
}
87+
}
88+
}

src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
using Bit.Billing.Constants;
2+
using Bit.Billing.Jobs;
23
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
4+
using Bit.Core.AdminConsole.Repositories;
5+
using Bit.Core.AdminConsole.Services;
36
using Bit.Core.Billing.Extensions;
47
using Bit.Core.Services;
8+
using Quartz;
59
using Event = Stripe.Event;
610
namespace Bit.Billing.Services.Implementations;
711

@@ -11,17 +15,26 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler
1115
private readonly IUserService _userService;
1216
private readonly IStripeEventUtilityService _stripeEventUtilityService;
1317
private readonly IOrganizationDisableCommand _organizationDisableCommand;
18+
private readonly IProviderRepository _providerRepository;
19+
private readonly IProviderService _providerService;
20+
private readonly ISchedulerFactory _schedulerFactory;
1421

1522
public SubscriptionDeletedHandler(
1623
IStripeEventService stripeEventService,
1724
IUserService userService,
1825
IStripeEventUtilityService stripeEventUtilityService,
19-
IOrganizationDisableCommand organizationDisableCommand)
26+
IOrganizationDisableCommand organizationDisableCommand,
27+
IProviderRepository providerRepository,
28+
IProviderService providerService,
29+
ISchedulerFactory schedulerFactory)
2030
{
2131
_stripeEventService = stripeEventService;
2232
_userService = userService;
2333
_stripeEventUtilityService = stripeEventUtilityService;
2434
_organizationDisableCommand = organizationDisableCommand;
35+
_providerRepository = providerRepository;
36+
_providerService = providerService;
37+
_schedulerFactory = schedulerFactory;
2538
}
2639

2740
/// <summary>
@@ -53,9 +66,38 @@ public async Task HandleAsync(Event parsedEvent)
5366

5467
await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.GetCurrentPeriodEnd());
5568
}
69+
else if (providerId.HasValue)
70+
{
71+
var provider = await _providerRepository.GetByIdAsync(providerId.Value);
72+
if (provider != null)
73+
{
74+
provider.Enabled = false;
75+
await _providerService.UpdateAsync(provider);
76+
77+
await QueueProviderOrganizationDisableJobAsync(providerId.Value, subscription.GetCurrentPeriodEnd());
78+
}
79+
}
5680
else if (userId.HasValue)
5781
{
5882
await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
5983
}
6084
}
85+
86+
private async Task QueueProviderOrganizationDisableJobAsync(Guid providerId, DateTime? expirationDate)
87+
{
88+
var scheduler = await _schedulerFactory.GetScheduler();
89+
90+
var job = JobBuilder.Create<ProviderOrganizationDisableJob>()
91+
.WithIdentity($"disable-provider-orgs-{providerId}", "provider-management")
92+
.UsingJobData("providerId", providerId.ToString())
93+
.UsingJobData("expirationDate", expirationDate?.ToString("O"))
94+
.Build();
95+
96+
var trigger = TriggerBuilder.Create()
97+
.WithIdentity($"disable-trigger-{providerId}", "provider-management")
98+
.StartNow()
99+
.Build();
100+
101+
await scheduler.ScheduleJob(job, trigger);
102+
}
61103
}

0 commit comments

Comments
 (0)