diff --git a/src/Core/Models/Business/CompleteSubscriptionUpdate.cs b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs
new file mode 100644
index 000000000..a1146cd2a
--- /dev/null
+++ b/src/Core/Models/Business/CompleteSubscriptionUpdate.cs
@@ -0,0 +1,329 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Exceptions;
+using Stripe;
+
+namespace Bit.Core.Models.Business;
+
+///
+/// A model representing the data required to upgrade from one subscription to another using a .
+///
+public class SubscriptionData
+{
+ public StaticStore.Plan Plan { get; init; }
+ public int PurchasedPasswordManagerSeats { get; init; }
+ public bool SubscribedToSecretsManager { get; set; }
+ public int? PurchasedSecretsManagerSeats { get; init; }
+ public int? PurchasedAdditionalSecretsManagerServiceAccounts { get; init; }
+ public int PurchasedAdditionalStorage { get; init; }
+}
+
+public class CompleteSubscriptionUpdate : SubscriptionUpdate
+{
+ private readonly SubscriptionData _currentSubscription;
+ private readonly SubscriptionData _updatedSubscription;
+
+ private readonly Dictionary _subscriptionUpdateMap = new();
+
+ private enum SubscriptionUpdateType
+ {
+ PasswordManagerSeats,
+ SecretsManagerSeats,
+ SecretsManagerServiceAccounts,
+ Storage
+ }
+
+ ///
+ /// A model used to generate the Stripe
+ /// necessary to both upgrade an organization's subscription and revert that upgrade
+ /// in the case of an error.
+ ///
+ /// The to upgrade.
+ /// The updates you want to apply to the organization's subscription.
+ public CompleteSubscriptionUpdate(
+ Organization organization,
+ SubscriptionData updatedSubscription)
+ {
+ _currentSubscription = GetSubscriptionDataFor(organization);
+ _updatedSubscription = updatedSubscription;
+ }
+
+ protected override List PlanIds => new()
+ {
+ GetPasswordManagerPlanId(_updatedSubscription.Plan),
+ _updatedSubscription.Plan.SecretsManager.StripeSeatPlanId,
+ _updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
+ _updatedSubscription.Plan.PasswordManager.StripeStoragePlanId
+ };
+
+ ///
+ /// Generates the necessary to revert an 's
+ /// upgrade in the case of an error.
+ ///
+ /// The organization's .
+ public override List RevertItemsOptions(Subscription subscription)
+ {
+ var subscriptionItemOptions = new List
+ {
+ GetPasswordManagerOptions(subscription, _updatedSubscription, _currentSubscription)
+ };
+
+ if (_updatedSubscription.SubscribedToSecretsManager || _currentSubscription.SubscribedToSecretsManager)
+ {
+ subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _updatedSubscription, _currentSubscription));
+
+ if (_updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||
+ _currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)
+ {
+ subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _updatedSubscription, _currentSubscription));
+ }
+ }
+
+ if (_updatedSubscription.PurchasedAdditionalStorage != 0 || _currentSubscription.PurchasedAdditionalStorage != 0)
+ {
+ subscriptionItemOptions.Add(GetStorageOptions(subscription, _updatedSubscription, _currentSubscription));
+ }
+
+ return subscriptionItemOptions;
+ }
+
+ /*
+ * This is almost certainly overkill. If we trust the data in the Vault DB, we should just be able to
+ * compare the _currentSubscription against the _updatedSubscription to see if there are any differences.
+ * However, for the sake of ensuring we're checking against the Stripe subscription itself, I'll leave this
+ * included for now.
+ */
+ ///
+ /// Checks whether the updates provided in the 's constructor
+ /// are actually different than the organization's current .
+ ///
+ /// The organization's .
+ public override bool UpdateNeeded(Subscription subscription)
+ {
+ var upgradeItemsOptions = UpgradeItemsOptions(subscription);
+
+ foreach (var subscriptionItemOptions in upgradeItemsOptions)
+ {
+ var success = _subscriptionUpdateMap.TryGetValue(subscriptionItemOptions.Price, out var updateType);
+
+ if (!success)
+ {
+ return false;
+ }
+
+ var updateNeeded = updateType switch
+ {
+ SubscriptionUpdateType.PasswordManagerSeats => ContainsUpdatesBetween(
+ GetPasswordManagerPlanId(_currentSubscription.Plan),
+ subscriptionItemOptions),
+ SubscriptionUpdateType.SecretsManagerSeats => ContainsUpdatesBetween(
+ _currentSubscription.Plan.SecretsManager.StripeSeatPlanId,
+ subscriptionItemOptions),
+ SubscriptionUpdateType.SecretsManagerServiceAccounts => ContainsUpdatesBetween(
+ _currentSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
+ subscriptionItemOptions),
+ SubscriptionUpdateType.Storage => ContainsUpdatesBetween(
+ _currentSubscription.Plan.PasswordManager.StripeStoragePlanId,
+ subscriptionItemOptions),
+ _ => false
+ };
+
+ if (updateNeeded)
+ {
+ return true;
+ }
+ }
+
+ return false;
+
+ bool ContainsUpdatesBetween(string currentPlanId, SubscriptionItemOptions options)
+ {
+ var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
+
+ return (subscriptionItem.Plan.Id != options.Plan && subscriptionItem.Price.Id != options.Plan) ||
+ subscriptionItem.Quantity != options.Quantity ||
+ subscriptionItem.Deleted != options.Deleted;
+ }
+ }
+
+ ///
+ /// Generates the necessary to upgrade an 's
+ /// .
+ ///
+ /// The organization's .
+ public override List UpgradeItemsOptions(Subscription subscription)
+ {
+ var subscriptionItemOptions = new List
+ {
+ GetPasswordManagerOptions(subscription, _currentSubscription, _updatedSubscription)
+ };
+
+ if (_currentSubscription.SubscribedToSecretsManager || _updatedSubscription.SubscribedToSecretsManager)
+ {
+ subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _currentSubscription, _updatedSubscription));
+
+ if (_currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||
+ _updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)
+ {
+ subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _currentSubscription, _updatedSubscription));
+ }
+ }
+
+ if (_currentSubscription.PurchasedAdditionalStorage != 0 || _updatedSubscription.PurchasedAdditionalStorage != 0)
+ {
+ subscriptionItemOptions.Add(GetStorageOptions(subscription, _currentSubscription, _updatedSubscription));
+ }
+
+ return subscriptionItemOptions;
+ }
+
+ private SubscriptionItemOptions GetPasswordManagerOptions(
+ Subscription subscription,
+ SubscriptionData from,
+ SubscriptionData to)
+ {
+ var currentPlanId = GetPasswordManagerPlanId(from.Plan);
+
+ var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
+
+ if (subscriptionItem == null)
+ {
+ throw new GatewayException("Could not find Password Manager subscription");
+ }
+
+ var updatedPlanId = GetPasswordManagerPlanId(to.Plan);
+
+ _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.PasswordManagerSeats;
+
+ return new SubscriptionItemOptions
+ {
+ Id = subscriptionItem.Id,
+ Price = updatedPlanId,
+ Quantity = IsNonSeatBasedPlan(to.Plan) ? 1 : to.PurchasedPasswordManagerSeats
+ };
+ }
+
+ private SubscriptionItemOptions GetSecretsManagerOptions(
+ Subscription subscription,
+ SubscriptionData from,
+ SubscriptionData to)
+ {
+ var currentPlanId = from.Plan?.SecretsManager?.StripeSeatPlanId;
+
+ var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)
+ ? FindSubscriptionItem(subscription, currentPlanId)
+ : null;
+
+ var updatedPlanId = to.Plan.SecretsManager.StripeSeatPlanId;
+
+ _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerSeats;
+
+ return new SubscriptionItemOptions
+ {
+ Id = subscriptionItem?.Id,
+ Price = updatedPlanId,
+ Quantity = to.PurchasedSecretsManagerSeats,
+ Deleted = subscriptionItem?.Id != null && to.PurchasedSecretsManagerSeats == 0
+ ? true
+ : null
+ };
+ }
+
+ private SubscriptionItemOptions GetServiceAccountsOptions(
+ Subscription subscription,
+ SubscriptionData from,
+ SubscriptionData to)
+ {
+ var currentPlanId = from.Plan?.SecretsManager?.StripeServiceAccountPlanId;
+
+ var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)
+ ? FindSubscriptionItem(subscription, currentPlanId)
+ : null;
+
+ var updatedPlanId = to.Plan.SecretsManager.StripeServiceAccountPlanId;
+
+ _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerServiceAccounts;
+
+ return new SubscriptionItemOptions
+ {
+ Id = subscriptionItem?.Id,
+ Price = updatedPlanId,
+ Quantity = to.PurchasedAdditionalSecretsManagerServiceAccounts,
+ Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalSecretsManagerServiceAccounts == 0
+ ? true
+ : null
+ };
+ }
+
+ private SubscriptionItemOptions GetStorageOptions(
+ Subscription subscription,
+ SubscriptionData from,
+ SubscriptionData to)
+ {
+ var currentPlanId = from.Plan.PasswordManager.StripeStoragePlanId;
+
+ var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
+
+ var updatedPlanId = to.Plan.PasswordManager.StripeStoragePlanId;
+
+ _subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.Storage;
+
+ return new SubscriptionItemOptions
+ {
+ Id = subscriptionItem?.Id,
+ Price = updatedPlanId,
+ Quantity = to.PurchasedAdditionalStorage,
+ Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalStorage == 0
+ ? true
+ : null
+ };
+ }
+
+ private static SubscriptionItem FindSubscriptionItem(Subscription subscription, string planId)
+ {
+ if (string.IsNullOrEmpty(planId))
+ {
+ return null;
+ }
+
+ var data = subscription.Items.Data;
+
+ var subscriptionItem = data.FirstOrDefault(item => item.Plan?.Id == planId) ?? data.FirstOrDefault(item => item.Price?.Id == planId);
+
+ return subscriptionItem;
+ }
+
+ private static string GetPasswordManagerPlanId(StaticStore.Plan plan)
+ => IsNonSeatBasedPlan(plan)
+ ? plan.PasswordManager.StripePlanId
+ : plan.PasswordManager.StripeSeatPlanId;
+
+ private static SubscriptionData GetSubscriptionDataFor(Organization organization)
+ {
+ var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
+
+ return new SubscriptionData
+ {
+ Plan = plan,
+ PurchasedPasswordManagerSeats = organization.Seats.HasValue
+ ? organization.Seats.Value - plan.PasswordManager.BaseSeats
+ : 0,
+ SubscribedToSecretsManager = organization.UseSecretsManager,
+ PurchasedSecretsManagerSeats = plan.SecretsManager is not null
+ ? organization.SmSeats - plan.SecretsManager.BaseSeats
+ : 0,
+ PurchasedAdditionalSecretsManagerServiceAccounts = plan.SecretsManager is not null
+ ? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount
+ : 0,
+ PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue
+ ? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) :
+ 0
+ };
+ }
+
+ private static bool IsNonSeatBasedPlan(StaticStore.Plan plan)
+ => plan.Type is
+ >= PlanType.FamiliesAnnually2019 and <= PlanType.EnterpriseAnnually2019
+ or PlanType.FamiliesAnnually
+ or PlanType.TeamsStarter;
+}
diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs
index 497a455d6..70106a10e 100644
--- a/src/Core/Models/Business/SubscriptionUpdate.cs
+++ b/src/Core/Models/Business/SubscriptionUpdate.cs
@@ -9,7 +9,7 @@ public abstract class SubscriptionUpdate
public abstract List RevertItemsOptions(Subscription subscription);
public abstract List UpgradeItemsOptions(Subscription subscription);
- public bool UpdateNeeded(Subscription subscription)
+ public virtual bool UpdateNeeded(Subscription subscription)
{
var upgradeItemsOptions = UpgradeItemsOptions(subscription);
foreach (var upgradeItemOptions in upgradeItemsOptions)
diff --git a/src/Core/Models/StaticStore/Plans/Enterprise2019Plan.cs b/src/Core/Models/StaticStore/Plans/Enterprise2019Plan.cs
index 7684b0897..802326def 100644
--- a/src/Core/Models/StaticStore/Plans/Enterprise2019Plan.cs
+++ b/src/Core/Models/StaticStore/Plans/Enterprise2019Plan.cs
@@ -31,8 +31,8 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
UsersGetPremium = true;
HasCustomPermissions = true;
- UpgradeSortOrder = 3;
- DisplaySortOrder = 3;
+ UpgradeSortOrder = 4;
+ DisplaySortOrder = 4;
LegacyYear = 2020;
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs b/src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs
index 4fa7eee97..d98432080 100644
--- a/src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs
+++ b/src/Core/Models/StaticStore/Plans/Enterprise2020Plan.cs
@@ -31,8 +31,8 @@ public record Enterprise2020Plan : Models.StaticStore.Plan
UsersGetPremium = true;
HasCustomPermissions = true;
- UpgradeSortOrder = 3;
- DisplaySortOrder = 3;
+ UpgradeSortOrder = 4;
+ DisplaySortOrder = 4;
LegacyYear = 2023;
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/EnterprisePlan.cs b/src/Core/Models/StaticStore/Plans/EnterprisePlan.cs
index 61eabc643..30242f49c 100644
--- a/src/Core/Models/StaticStore/Plans/EnterprisePlan.cs
+++ b/src/Core/Models/StaticStore/Plans/EnterprisePlan.cs
@@ -31,8 +31,8 @@ public record EnterprisePlan : Models.StaticStore.Plan
UsersGetPremium = true;
HasCustomPermissions = true;
- UpgradeSortOrder = 3;
- DisplaySortOrder = 3;
+ UpgradeSortOrder = 4;
+ DisplaySortOrder = 4;
PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);
SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/Teams2019Plan.cs b/src/Core/Models/StaticStore/Plans/Teams2019Plan.cs
index d81a015de..ce53354f2 100644
--- a/src/Core/Models/StaticStore/Plans/Teams2019Plan.cs
+++ b/src/Core/Models/StaticStore/Plans/Teams2019Plan.cs
@@ -24,8 +24,8 @@ public record Teams2019Plan : Models.StaticStore.Plan
HasApi = true;
UsersGetPremium = true;
- UpgradeSortOrder = 2;
- DisplaySortOrder = 2;
+ UpgradeSortOrder = 3;
+ DisplaySortOrder = 3;
LegacyYear = 2020;
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/Teams2020Plan.cs b/src/Core/Models/StaticStore/Plans/Teams2020Plan.cs
index 680a5deec..e040edc88 100644
--- a/src/Core/Models/StaticStore/Plans/Teams2020Plan.cs
+++ b/src/Core/Models/StaticStore/Plans/Teams2020Plan.cs
@@ -24,8 +24,8 @@ public record Teams2020Plan : Models.StaticStore.Plan
HasApi = true;
UsersGetPremium = true;
- UpgradeSortOrder = 2;
- DisplaySortOrder = 2;
+ UpgradeSortOrder = 3;
+ DisplaySortOrder = 3;
LegacyYear = 2023;
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/TeamsPlan.cs b/src/Core/Models/StaticStore/Plans/TeamsPlan.cs
index 77482d10f..d181f6274 100644
--- a/src/Core/Models/StaticStore/Plans/TeamsPlan.cs
+++ b/src/Core/Models/StaticStore/Plans/TeamsPlan.cs
@@ -24,8 +24,8 @@ public record TeamsPlan : Models.StaticStore.Plan
HasApi = true;
UsersGetPremium = true;
- UpgradeSortOrder = 2;
- DisplaySortOrder = 2;
+ UpgradeSortOrder = 3;
+ DisplaySortOrder = 3;
PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);
SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);
diff --git a/src/Core/Models/StaticStore/Plans/TeamsStarterPlan.cs b/src/Core/Models/StaticStore/Plans/TeamsStarterPlan.cs
index 1b9b1b876..d00fec8f8 100644
--- a/src/Core/Models/StaticStore/Plans/TeamsStarterPlan.cs
+++ b/src/Core/Models/StaticStore/Plans/TeamsStarterPlan.cs
@@ -64,6 +64,7 @@ public record TeamsStarterPlan : Plan
HasAdditionalStorageOption = true;
StripePlanId = "teams-org-starter";
+ StripeStoragePlanId = "storage-gb-monthly";
AdditionalStoragePricePerGb = 0.5M;
}
}
diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
index 08b33e666..0023484bf 100644
--- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
+++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs
@@ -97,11 +97,6 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
throw new BadRequestException("You cannot upgrade to this plan.");
}
- if (existingPlan.Type != PlanType.Free)
- {
- throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
- }
-
_organizationService.ValidatePasswordManagerPlan(newPlan, upgrade);
if (upgrade.UseSecretsManager)
@@ -226,8 +221,16 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
}
else
{
- // TODO: Update existing sub
- throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
+ paymentIntentClientSecret = await _paymentService.AdjustSubscription(
+ organization,
+ newPlan,
+ upgrade.AdditionalSeats,
+ upgrade.UseSecretsManager,
+ upgrade.AdditionalSmSeats,
+ upgrade.AdditionalServiceAccounts,
+ upgrade.AdditionalStorageGb);
+
+ success = string.IsNullOrEmpty(paymentIntentClientSecret);
}
organization.BusinessName = upgrade.BusinessName;
diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs
index 2385155d3..04526268f 100644
--- a/src/Core/Services/IPaymentService.cs
+++ b/src/Core/Services/IPaymentService.cs
@@ -18,6 +18,15 @@ public interface IPaymentService
Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
short additionalStorageGb, TaxInfo taxInfo);
+ Task AdjustSubscription(
+ Organization organization,
+ Plan updatedPlan,
+ int newlyPurchasedPasswordManagerSeats,
+ bool subscribedToSecretsManager,
+ int? newlyPurchasedSecretsManagerSeats,
+ int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
+ int newlyPurchasedAdditionalStorage,
+ DateTime? prorationDate = null);
Task AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
Task AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs
index 4de12c6af..78b54b7a1 100644
--- a/src/Core/Services/Implementations/StripePaymentService.cs
+++ b/src/Core/Services/Implementations/StripePaymentService.cs
@@ -741,7 +741,6 @@ public class StripePaymentService : IPaymentService
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
{
// remember, when in doubt, throw
-
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
if (sub == null)
{
@@ -860,6 +859,30 @@ public class StripePaymentService : IPaymentService
return paymentIntentClientSecret;
}
+ public Task AdjustSubscription(
+ Organization organization,
+ StaticStore.Plan updatedPlan,
+ int newlyPurchasedPasswordManagerSeats,
+ bool subscribedToSecretsManager,
+ int? newlyPurchasedSecretsManagerSeats,
+ int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
+ int newlyPurchasedAdditionalStorage,
+ DateTime? prorationDate = null)
+ => FinalizeSubscriptionChangeAsync(
+ organization,
+ new CompleteSubscriptionUpdate(
+ organization,
+ new SubscriptionData
+ {
+ Plan = updatedPlan,
+ PurchasedPasswordManagerSeats = newlyPurchasedPasswordManagerSeats,
+ SubscribedToSecretsManager = subscribedToSecretsManager,
+ PurchasedSecretsManagerSeats = newlyPurchasedSecretsManagerSeats,
+ PurchasedAdditionalSecretsManagerServiceAccounts = newlyPurchasedAdditionalSecretsManagerServiceAccounts,
+ PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
+ }),
+ prorationDate);
+
public Task AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
{
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
diff --git a/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs
index 297dbe893..ef3889c6d 100644
--- a/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs
+++ b/test/Core.Test/AdminConsole/AutoFixture/OrganizationFixtures.cs
@@ -147,6 +147,40 @@ public class SecretsManagerOrganizationCustomization : ICustomization
}
}
+internal class TeamsStarterOrganizationCustomization : ICustomization
+{
+ public void Customize(IFixture fixture)
+ {
+ var organizationId = Guid.NewGuid();
+ const PlanType planType = PlanType.TeamsStarter;
+
+ fixture.Customize(composer =>
+ composer
+ .With(organization => organization.Id, organizationId)
+ .With(organization => organization.PlanType, planType)
+ .With(organization => organization.Seats, 10)
+ .Without(organization => organization.MaxStorageGb));
+ }
+}
+
+internal class TeamsMonthlyWithAddOnsOrganizationCustomization : ICustomization
+{
+ public void Customize(IFixture fixture)
+ {
+ var organizationId = Guid.NewGuid();
+ const PlanType planType = PlanType.TeamsMonthly;
+
+ fixture.Customize(composer =>
+ composer
+ .With(organization => organization.Id, organizationId)
+ .With(organization => organization.PlanType, planType)
+ .With(organization => organization.Seats, 20)
+ .With(organization => organization.UseSecretsManager, true)
+ .With(organization => organization.SmSeats, 5)
+ .With(organization => organization.SmServiceAccounts, 53));
+ }
+}
+
internal class OrganizationCustomizeAttribute : BitCustomizeAttribute
{
public bool UseGroups { get; set; }
@@ -189,6 +223,16 @@ internal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttrib
new SecretsManagerOrganizationCustomization();
}
+internal class TeamsStarterOrganizationCustomizeAttribute : BitCustomizeAttribute
+{
+ public override ICustomization GetCustomization() => new TeamsStarterOrganizationCustomization();
+}
+
+internal class TeamsMonthlyWithAddOnsOrganizationCustomizeAttribute : BitCustomizeAttribute
+{
+ public override ICustomization GetCustomization() => new TeamsMonthlyWithAddOnsOrganizationCustomization();
+}
+
internal class EphemeralDataProtectionCustomization : ICustomization
{
public void Customize(IFixture fixture)
diff --git a/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs
new file mode 100644
index 000000000..03d8d8382
--- /dev/null
+++ b/test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs
@@ -0,0 +1,530 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Models.Business;
+using Bit.Core.Test.AutoFixture.OrganizationFixtures;
+using Bit.Core.Utilities;
+using Bit.Test.Common.AutoFixture.Attributes;
+using Stripe;
+using Xunit;
+
+namespace Bit.Core.Test.Models.Business;
+
+public class CompleteSubscriptionUpdateTests
+{
+ [Theory]
+ [BitAutoData]
+ [TeamsStarterOrganizationCustomize]
+ public void UpgradeItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "subscription_item",
+ Price = new Price { Id = teamsStarterPlan.PasswordManager.StripePlanId },
+ Quantity = 1
+ }
+ }
+ }
+ };
+
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = teamsMonthlyPlan,
+ PurchasedPasswordManagerSeats = 20
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
+
+ Assert.Single(upgradeItemOptions);
+
+ var passwordManagerOptions = upgradeItemOptions.First();
+
+ Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);
+ Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+ }
+
+ [Theory]
+ [BitAutoData]
+ [TeamsMonthlyWithAddOnsOrganizationCustomize]
+ public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ // 5 purchased, 1 base
+ organization.MaxStorageGb = 6;
+
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "password_manager_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
+ Quantity = organization.Seats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },
+ Quantity = organization.SmSeats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_service_accounts_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
+ Quantity = organization.SmServiceAccounts!.Value
+ },
+ new ()
+ {
+ Id = "password_manager_storage_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },
+ Quantity = organization.Storage!.Value
+ }
+ }
+ }
+ };
+
+ var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = enterpriseMonthlyPlan,
+ PurchasedPasswordManagerSeats = 50,
+ SubscribedToSecretsManager = true,
+ PurchasedSecretsManagerSeats = 30,
+ PurchasedAdditionalSecretsManagerServiceAccounts = 10,
+ PurchasedAdditionalStorage = 10
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
+
+ Assert.Equal(4, upgradeItemOptions.Count);
+
+ var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);
+
+ var passwordManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
+
+ Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+
+ var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);
+
+ var secretsManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
+
+ Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);
+ Assert.Null(secretsManagerOptions.Deleted);
+
+ var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
+
+ var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
+ item.Id == "secrets_manager_service_accounts_subscription_item");
+
+ Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);
+ Assert.Null(serviceAccountsOptions.Deleted);
+
+ var storageOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);
+
+ var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
+
+ Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);
+ Assert.Null(storageOptions.Deleted);
+ }
+
+ [Theory]
+ [BitAutoData]
+ [TeamsMonthlyWithAddOnsOrganizationCustomize]
+ public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ // 5 purchased, 1 base
+ organization.MaxStorageGb = 6;
+
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "password_manager_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
+ Quantity = organization.Seats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },
+ Quantity = organization.SmSeats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_service_accounts_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
+ Quantity = organization.SmServiceAccounts!.Value
+ },
+ new ()
+ {
+ Id = "password_manager_storage_subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },
+ Quantity = organization.Storage!.Value
+ }
+ }
+ }
+ };
+
+ var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = enterpriseMonthlyPlan,
+ PurchasedPasswordManagerSeats = 50,
+ SubscribedToSecretsManager = false,
+ PurchasedSecretsManagerSeats = 0,
+ PurchasedAdditionalSecretsManagerServiceAccounts = 0,
+ PurchasedAdditionalStorage = 10
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
+
+ Assert.Equal(4, upgradeItemOptions.Count);
+
+ var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);
+
+ var passwordManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
+
+ Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+
+ var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);
+
+ var secretsManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
+
+ Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);
+ Assert.True(secretsManagerOptions.Deleted);
+
+ var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
+
+ var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
+ item.Id == "secrets_manager_service_accounts_subscription_item");
+
+ Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);
+ Assert.True(serviceAccountsOptions.Deleted);
+
+ var storageOptions = upgradeItemOptions.FirstOrDefault(options =>
+ options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);
+
+ var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
+
+ Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
+ Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
+ Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);
+ Assert.Null(storageOptions.Deleted);
+ }
+
+ [Theory]
+ [BitAutoData]
+ [TeamsStarterOrganizationCustomize]
+ public void RevertItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "subscription_item",
+ Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
+ Quantity = 20
+ }
+ }
+ }
+ };
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = teamsMonthlyPlan,
+ PurchasedPasswordManagerSeats = 20
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
+
+ Assert.Single(revertItemOptions);
+
+ var passwordManagerOptions = revertItemOptions.First();
+
+ Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);
+ Assert.Equal(teamsStarterPlan.PasswordManager.StripePlanId, passwordManagerOptions.Price);
+ Assert.Equal(1, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+ }
+
+ [Theory]
+ [BitAutoData]
+ [TeamsMonthlyWithAddOnsOrganizationCustomize]
+ public void RevertItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ // 5 purchased, 1 base
+ organization.MaxStorageGb = 6;
+
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+ var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "password_manager_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },
+ Quantity = organization.Seats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },
+ Quantity = organization.SmSeats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_service_accounts_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
+ Quantity = organization.SmServiceAccounts!.Value
+ },
+ new ()
+ {
+ Id = "password_manager_storage_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },
+ Quantity = organization.Storage!.Value
+ }
+ }
+ }
+ };
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = enterpriseMonthlyPlan,
+ PurchasedPasswordManagerSeats = 50,
+ SubscribedToSecretsManager = true,
+ PurchasedSecretsManagerSeats = 30,
+ PurchasedAdditionalSecretsManagerServiceAccounts = 10,
+ PurchasedAdditionalStorage = 10
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
+
+ Assert.Equal(4, revertItemOptions.Count);
+
+ var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);
+
+ var passwordManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
+
+ Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
+ Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+
+ var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);
+
+ var secretsManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
+
+ Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
+ Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);
+ Assert.Null(secretsManagerOptions.Deleted);
+
+ var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
+
+ var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
+ item.Id == "secrets_manager_service_accounts_subscription_item");
+
+ Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
+ Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);
+ Assert.Null(serviceAccountsOptions.Deleted);
+
+ var storageOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);
+
+ var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
+
+ Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
+ Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);
+ Assert.Null(storageOptions.Deleted);
+ }
+
+ [Theory]
+ [BitAutoData]
+ [TeamsMonthlyWithAddOnsOrganizationCustomize]
+ public void RevertItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(
+ Organization organization)
+ {
+ // 5 purchased, 1 base
+ organization.MaxStorageGb = 6;
+
+ var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
+ var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
+
+ var subscription = new Subscription
+ {
+ Items = new StripeList
+ {
+ Data = new List
+ {
+ new ()
+ {
+ Id = "password_manager_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },
+ Quantity = organization.Seats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },
+ Quantity = organization.SmSeats!.Value
+ },
+ new ()
+ {
+ Id = "secrets_manager_service_accounts_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
+ Quantity = organization.SmServiceAccounts!.Value
+ },
+ new ()
+ {
+ Id = "password_manager_storage_subscription_item",
+ Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },
+ Quantity = organization.Storage!.Value
+ }
+ }
+ }
+ };
+
+ var updatedSubscriptionData = new SubscriptionData
+ {
+ Plan = enterpriseMonthlyPlan,
+ PurchasedPasswordManagerSeats = 50,
+ SubscribedToSecretsManager = false,
+ PurchasedSecretsManagerSeats = 0,
+ PurchasedAdditionalSecretsManagerServiceAccounts = 0,
+ PurchasedAdditionalStorage = 10
+ };
+
+ var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
+
+ var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
+
+ Assert.Equal(4, revertItemOptions.Count);
+
+ var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);
+
+ var passwordManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
+
+ Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
+ Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);
+ Assert.Null(passwordManagerOptions.Deleted);
+
+ var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);
+
+ var secretsManagerSubscriptionItem =
+ subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
+
+ Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
+ Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);
+ Assert.Null(secretsManagerOptions.Deleted);
+
+ var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
+
+ var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
+ item.Id == "secrets_manager_service_accounts_subscription_item");
+
+ Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
+ Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);
+ Assert.Null(serviceAccountsOptions.Deleted);
+
+ var storageOptions = revertItemOptions.FirstOrDefault(options =>
+ options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);
+
+ var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
+
+ Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
+ Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
+ Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);
+ Assert.Null(storageOptions.Deleted);
+ }
+}
diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs
index 44f07a7c9..d0d11acf7 100644
--- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs
+++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs
@@ -63,29 +63,6 @@ public class UpgradeOrganizationPlanCommandTests
Assert.Contains("already on this plan", exception.Message);
}
- [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
- public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
- SutProvider sutProvider)
- {
- sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization);
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
- Assert.Contains("can only upgrade", exception.Message);
- }
-
- [Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
- public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
- SutProvider sutProvider)
- {
- upgrade.UseSecretsManager = true;
- upgrade.AdditionalSmSeats = 10;
- upgrade.AdditionalServiceAccounts = 10;
- sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization);
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
- Assert.Contains("can only upgrade", exception.Message);
- }
-
[Theory]
[FreeOrganizationUpgradeCustomize, BitAutoData]
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
@@ -99,6 +76,41 @@ public class UpgradeOrganizationPlanCommandTests
await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync(organization);
}
+ [Theory]
+ [BitAutoData(PlanType.TeamsStarter)]
+ [BitAutoData(PlanType.TeamsMonthly)]
+ [BitAutoData(PlanType.TeamsAnnually)]
+ [BitAutoData(PlanType.EnterpriseMonthly)]
+ [BitAutoData(PlanType.EnterpriseAnnually)]
+ public async Task UpgradePlan_FromFamilies_Passes(
+ PlanType planType,
+ Organization organization,
+ OrganizationUpgrade organizationUpgrade,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization);
+
+ organization.PlanType = PlanType.FamiliesAnnually;
+
+ organizationUpgrade.AdditionalSeats = 30;
+ organizationUpgrade.UseSecretsManager = true;
+ organizationUpgrade.AdditionalSmSeats = 20;
+ organizationUpgrade.AdditionalServiceAccounts = 5;
+ organizationUpgrade.AdditionalStorageGb = 3;
+ organizationUpgrade.Plan = planType;
+
+ await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
+ await sutProvider.GetDependency().Received(1).AdjustSubscription(
+ organization,
+ StaticStore.GetPlan(planType),
+ organizationUpgrade.AdditionalSeats,
+ organizationUpgrade.UseSecretsManager,
+ organizationUpgrade.AdditionalSmSeats,
+ 5,
+ 3);
+ await sutProvider.GetDependency().Received(1).ReplaceAndUpdateCacheAsync(organization);
+ }
+
[Theory, FreeOrganizationUpgradeCustomize]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
@@ -130,7 +142,6 @@ public class UpgradeOrganizationPlanCommandTests
Assert.NotNull(result.Item2);
}
-
[Theory, FreeOrganizationUpgradeCustomize]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]