1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-07 00:21:32 +01:00

[PM-15807]Move subscription to 'canceled' 7 days after unpaid (#5221)

* Changes to implement the cancel job

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Resolve the Dependency issues

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* changes when open invoices is more than 10

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

* Move the package reference to ore

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>

---------

Signed-off-by: Cy Okeke <cokeke@bitwarden.com>
This commit is contained in:
cyprain-okeke 2025-01-24 12:02:13 +01:00 committed by GitHub
parent ca21758492
commit ef32e80725
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 107 additions and 1 deletions

View File

@ -32,5 +32,6 @@ public class JobsHostedService : BaseJobsHostedService
public static void AddJobsServices(IServiceCollection services)
{
services.AddTransient<AliveJob>();
services.AddTransient<SubscriptionCancellationJob>();
}
}

View File

@ -0,0 +1,58 @@
using Bit.Billing.Services;
using Bit.Core.Repositories;
using Quartz;
using Stripe;
namespace Bit.Billing.Jobs;
public class SubscriptionCancellationJob(
IStripeFacade stripeFacade,
IOrganizationRepository organizationRepository)
: IJob
{
public async Task Execute(IJobExecutionContext context)
{
var subscriptionId = context.MergedJobDataMap.GetString("subscriptionId");
var organizationId = new Guid(context.MergedJobDataMap.GetString("organizationId") ?? string.Empty);
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null || organization.Enabled)
{
// Organization was deleted or re-enabled by CS, skip cancellation
return;
}
var subscription = await stripeFacade.GetSubscription(subscriptionId);
if (subscription?.Status != "unpaid")
{
// Subscription is no longer unpaid, skip cancellation
return;
}
// Cancel the subscription
await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
// Void any open invoices
var options = new InvoiceListOptions
{
Status = "open",
Subscription = subscriptionId,
Limit = 100
};
var invoices = await stripeFacade.ListInvoices(options);
foreach (var invoice in invoices)
{
await stripeFacade.VoidInvoice(invoice.Id);
}
while (invoices.HasMore)
{
options.StartingAfter = invoices.Data.Last().Id;
invoices = await stripeFacade.ListInvoices(options);
foreach (var invoice in invoices)
{
await stripeFacade.VoidInvoice(invoice.Id);
}
}
}
}

View File

@ -1,9 +1,12 @@
using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Core;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Quartz;
using Stripe;
using Event = Stripe.Event;
@ -19,6 +22,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private readonly IUserService _userService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISchedulerFactory _schedulerFactory;
private readonly IFeatureService _featureService;
public SubscriptionUpdatedHandler(
IStripeEventService stripeEventService,
@ -28,7 +33,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
IUserService userService,
IPushNotificationService pushNotificationService,
IOrganizationRepository organizationRepository)
IOrganizationRepository organizationRepository,
ISchedulerFactory schedulerFactory,
IFeatureService featureService)
{
_stripeEventService = stripeEventService;
_stripeEventUtilityService = stripeEventUtilityService;
@ -38,6 +45,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
_userService = userService;
_pushNotificationService = pushNotificationService;
_organizationRepository = organizationRepository;
_schedulerFactory = schedulerFactory;
_featureService = featureService;
}
/// <summary>
@ -55,6 +64,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
when organizationId.HasValue:
{
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
if (subscription.Status == StripeSubscriptionStatus.Unpaid)
{
await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value);
}
break;
}
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired:
@ -183,4 +196,27 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id);
}
}
private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId)
{
var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert);
if (isResellerManagedOrgAlertEnabled)
{
var scheduler = await _schedulerFactory.GetScheduler();
var job = JobBuilder.Create<SubscriptionCancellationJob>()
.WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations")
.UsingJobData("subscriptionId", subscriptionId)
.UsingJobData("organizationId", organizationId.ToString())
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations")
.StartAt(DateTimeOffset.UtcNow.AddDays(7))
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
}

View File

@ -9,6 +9,7 @@ using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Quartz;
using Stripe;
namespace Bit.Billing;
@ -101,6 +102,13 @@ public class Startup
services.AddScoped<IStripeEventService, StripeEventService>();
services.AddScoped<IProviderEventService, ProviderEventService>();
// Add Quartz services first
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
});
services.AddQuartzHostedService();
// Jobs service
Jobs.JobsHostedService.AddJobsServices(services);
services.AddHostedService<Jobs.JobsHostedService>();

View File

@ -67,6 +67,9 @@
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
<PackageReference Include="Quartz" Version="3.13.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
</ItemGroup>
<ItemGroup>