From c60f260c0f8e2da0cf0f38de1c54f3e0381ba69a Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:30:23 -0500 Subject: [PATCH] [AC-1754] Provide upgrade flow for paid organizations (#3468) * wip * Add CompleteSubscriptionUpdate * Add AdjustSubscription to PaymentService * Use PaymentService.AdjustSubscription in UpgradeOrganizationPlanCommand * Add CompleteSubscriptionUpdateTests * Remove unused changes * Update UpgradeOrganizationPlanCommandTests * Fixing missing usings after master merge * Defects: AC-1958, AC-1959 * Allow user to unsubscribe from Secrets Manager and Storage during upgrade * Handled null exception when upgrading away from a plan that doesn't allow secrets manager * Resolved issue where Teams Starter couldn't increase storage --------- Co-authored-by: Conner Turnbull Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- .../Business/CompleteSubscriptionUpdate.cs | 329 +++++++++++ .../Models/Business/SubscriptionUpdate.cs | 2 +- .../StaticStore/Plans/Enterprise2019Plan.cs | 4 +- .../StaticStore/Plans/Enterprise2020Plan.cs | 4 +- .../StaticStore/Plans/EnterprisePlan.cs | 4 +- .../Models/StaticStore/Plans/Teams2019Plan.cs | 4 +- .../Models/StaticStore/Plans/Teams2020Plan.cs | 4 +- .../Models/StaticStore/Plans/TeamsPlan.cs | 4 +- .../StaticStore/Plans/TeamsStarterPlan.cs | 1 + .../UpgradeOrganizationPlanCommand.cs | 17 +- src/Core/Services/IPaymentService.cs | 9 + .../Implementations/StripePaymentService.cs | 25 +- .../AutoFixture/OrganizationFixtures.cs | 44 ++ .../CompleteSubscriptionUpdateTests.cs | 530 ++++++++++++++++++ .../UpgradeOrganizationPlanCommandTests.cs | 59 +- 15 files changed, 995 insertions(+), 45 deletions(-) create mode 100644 src/Core/Models/Business/CompleteSubscriptionUpdate.cs create mode 100644 test/Core.Test/Models/Business/CompleteSubscriptionUpdateTests.cs 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)]