mirror of
https://github.com/bitwarden/server
synced 2025-12-31 07:33:43 +00:00
[PM-21421] Support legacy > current plan transition when resubscribing (#6728)
* Refactor RestartSubscriptionCommand to support legacy > modern plan transition * Run dotnet format * Claude feedback * Claude feedback
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OneOf.Types;
|
||||
using Stripe;
|
||||
|
||||
@@ -21,14 +22,14 @@ public interface IRestartSubscriptionCommand
|
||||
}
|
||||
|
||||
public class RestartSubscriptionCommand(
|
||||
ILogger<RestartSubscriptionCommand> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
IUserRepository userRepository) : IRestartSubscriptionCommand
|
||||
ISubscriberService subscriberService) : BaseBillingCommand<RestartSubscriptionCommand>(logger), IRestartSubscriptionCommand
|
||||
{
|
||||
public async Task<BillingCommandResult<None>> Run(
|
||||
ISubscriber subscriber)
|
||||
public Task<BillingCommandResult<None>> Run(
|
||||
ISubscriber subscriber) => HandleAsync<None>(async () =>
|
||||
{
|
||||
var existingSubscription = await subscriberService.GetSubscription(subscriber);
|
||||
|
||||
@@ -37,56 +38,147 @@ public class RestartSubscriptionCommand(
|
||||
return new BadRequest("Cannot restart a subscription that is not canceled.");
|
||||
}
|
||||
|
||||
await RestartSubscriptionAsync(subscriber, existingSubscription);
|
||||
|
||||
return new None();
|
||||
});
|
||||
|
||||
private Task RestartSubscriptionAsync(
|
||||
ISubscriber subscriber,
|
||||
Subscription canceledSubscription) => subscriber switch
|
||||
{
|
||||
Organization organization => RestartOrganizationSubscriptionAsync(organization, canceledSubscription),
|
||||
_ => throw new NotSupportedException("Only organization subscriptions can be restarted")
|
||||
};
|
||||
|
||||
private async Task RestartOrganizationSubscriptionAsync(
|
||||
Organization organization,
|
||||
Subscription canceledSubscription)
|
||||
{
|
||||
var plans = await pricingClient.ListPlans();
|
||||
|
||||
var oldPlan = plans.FirstOrDefault(plan => plan.Type == organization.PlanType);
|
||||
|
||||
if (oldPlan == null)
|
||||
{
|
||||
throw new ConflictException("Could not find plan for organization's plan type");
|
||||
}
|
||||
|
||||
var newPlan = oldPlan.Disabled
|
||||
? plans.FirstOrDefault(plan =>
|
||||
plan.ProductTier == oldPlan.ProductTier &&
|
||||
plan.IsAnnual == oldPlan.IsAnnual &&
|
||||
!plan.Disabled)
|
||||
: oldPlan;
|
||||
|
||||
if (newPlan == null)
|
||||
{
|
||||
throw new ConflictException("Could not find the current, enabled plan for organization's tier and cadence");
|
||||
}
|
||||
|
||||
if (newPlan.Type != oldPlan.Type)
|
||||
{
|
||||
organization.PlanType = newPlan.Type;
|
||||
organization.Plan = newPlan.Name;
|
||||
organization.SelfHost = newPlan.HasSelfHost;
|
||||
organization.UsePolicies = newPlan.HasPolicies;
|
||||
organization.UseGroups = newPlan.HasGroups;
|
||||
organization.UseDirectory = newPlan.HasDirectory;
|
||||
organization.UseEvents = newPlan.HasEvents;
|
||||
organization.UseTotp = newPlan.HasTotp;
|
||||
organization.Use2fa = newPlan.Has2fa;
|
||||
organization.UseApi = newPlan.HasApi;
|
||||
organization.UseSso = newPlan.HasSso;
|
||||
organization.UseOrganizationDomains = newPlan.HasOrganizationDomains;
|
||||
organization.UseKeyConnector = newPlan.HasKeyConnector;
|
||||
organization.UseScim = newPlan.HasScim;
|
||||
organization.UseResetPassword = newPlan.HasResetPassword;
|
||||
organization.UsersGetPremium = newPlan.UsersGetPremium;
|
||||
organization.UseCustomPermissions = newPlan.HasCustomPermissions;
|
||||
}
|
||||
|
||||
var items = new List<SubscriptionItemOptions>();
|
||||
|
||||
// Password Manager
|
||||
var passwordManagerItem = canceledSubscription.Items.FirstOrDefault(item =>
|
||||
item.Price.Id == (oldPlan.HasNonSeatBasedPasswordManagerPlan()
|
||||
? oldPlan.PasswordManager.StripePlanId
|
||||
: oldPlan.PasswordManager.StripeSeatPlanId));
|
||||
|
||||
if (passwordManagerItem == null)
|
||||
{
|
||||
throw new ConflictException("Organization's subscription does not have a Password Manager subscription item.");
|
||||
}
|
||||
|
||||
items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = newPlan.HasNonSeatBasedPasswordManagerPlan() ? newPlan.PasswordManager.StripePlanId : newPlan.PasswordManager.StripeSeatPlanId,
|
||||
Quantity = passwordManagerItem.Quantity
|
||||
});
|
||||
|
||||
// Storage
|
||||
var storageItem = canceledSubscription.Items.FirstOrDefault(
|
||||
item => item.Price.Id == oldPlan.PasswordManager.StripeStoragePlanId);
|
||||
|
||||
if (storageItem != null)
|
||||
{
|
||||
items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = newPlan.PasswordManager.StripeStoragePlanId,
|
||||
Quantity = storageItem.Quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Secrets Manager & Service Accounts
|
||||
var secretsManagerItem = oldPlan.SecretsManager != null
|
||||
? canceledSubscription.Items.FirstOrDefault(item =>
|
||||
item.Price.Id == oldPlan.SecretsManager.StripeSeatPlanId)
|
||||
: null;
|
||||
|
||||
var serviceAccountsItem = oldPlan.SecretsManager != null
|
||||
? canceledSubscription.Items.FirstOrDefault(item =>
|
||||
item.Price.Id == oldPlan.SecretsManager.StripeServiceAccountPlanId)
|
||||
: null;
|
||||
|
||||
if (newPlan.SecretsManager != null)
|
||||
{
|
||||
if (secretsManagerItem != null)
|
||||
{
|
||||
items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = newPlan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = secretsManagerItem.Quantity
|
||||
});
|
||||
}
|
||||
|
||||
if (serviceAccountsItem != null)
|
||||
{
|
||||
items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Price = newPlan.SecretsManager.StripeServiceAccountPlanId,
|
||||
Quantity = serviceAccountsItem.Quantity
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var options = new SubscriptionCreateOptions
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||
CollectionMethod = CollectionMethod.ChargeAutomatically,
|
||||
Customer = existingSubscription.CustomerId,
|
||||
Items = existingSubscription.Items.Select(subscriptionItem => new SubscriptionItemOptions
|
||||
{
|
||||
Price = subscriptionItem.Price.Id,
|
||||
Quantity = subscriptionItem.Quantity
|
||||
}).ToList(),
|
||||
Metadata = existingSubscription.Metadata,
|
||||
Customer = canceledSubscription.CustomerId,
|
||||
Items = items,
|
||||
Metadata = canceledSubscription.Metadata,
|
||||
OffSession = true,
|
||||
TrialPeriodDays = 0
|
||||
};
|
||||
|
||||
var subscription = await stripeAdapter.CreateSubscriptionAsync(options);
|
||||
await EnableAsync(subscriber, subscription);
|
||||
return new None();
|
||||
}
|
||||
|
||||
private async Task EnableAsync(ISubscriber subscriber, Subscription subscription)
|
||||
{
|
||||
switch (subscriber)
|
||||
{
|
||||
case Organization organization:
|
||||
{
|
||||
organization.GatewaySubscriptionId = subscription.Id;
|
||||
organization.Enabled = true;
|
||||
organization.ExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
organization.RevisionDate = DateTime.UtcNow;
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
break;
|
||||
}
|
||||
case Provider provider:
|
||||
{
|
||||
provider.GatewaySubscriptionId = subscription.Id;
|
||||
provider.Enabled = true;
|
||||
provider.RevisionDate = DateTime.UtcNow;
|
||||
await providerRepository.ReplaceAsync(provider);
|
||||
break;
|
||||
}
|
||||
case User user:
|
||||
{
|
||||
user.GatewaySubscriptionId = subscription.Id;
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
user.RevisionDate = DateTime.UtcNow;
|
||||
await userRepository.ReplaceAsync(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
organization.GatewaySubscriptionId = subscription.Id;
|
||||
organization.Enabled = true;
|
||||
organization.ExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
organization.RevisionDate = DateTime.UtcNow;
|
||||
|
||||
await organizationRepository.ReplaceAsync(organization);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user