mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[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 <cturnbull@bitwarden.com> Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
This commit is contained in:
parent
cf4d8a4f92
commit
c60f260c0f
329
src/Core/Models/Business/CompleteSubscriptionUpdate.cs
Normal file
329
src/Core/Models/Business/CompleteSubscriptionUpdate.cs
Normal file
@ -0,0 +1,329 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
/// <summary>
|
||||
/// A model representing the data required to upgrade from one subscription to another using a <see cref="CompleteSubscriptionUpdate"/>.
|
||||
/// </summary>
|
||||
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<string, SubscriptionUpdateType> _subscriptionUpdateMap = new();
|
||||
|
||||
private enum SubscriptionUpdateType
|
||||
{
|
||||
PasswordManagerSeats,
|
||||
SecretsManagerSeats,
|
||||
SecretsManagerServiceAccounts,
|
||||
Storage
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A model used to generate the Stripe <see cref="SubscriptionItemOptions"/>
|
||||
/// necessary to both upgrade an organization's subscription and revert that upgrade
|
||||
/// in the case of an error.
|
||||
/// </summary>
|
||||
/// <param name="organization">The <see cref="Organization"/> to upgrade.</param>
|
||||
/// <param name="updatedSubscription">The updates you want to apply to the organization's subscription.</param>
|
||||
public CompleteSubscriptionUpdate(
|
||||
Organization organization,
|
||||
SubscriptionData updatedSubscription)
|
||||
{
|
||||
_currentSubscription = GetSubscriptionDataFor(organization);
|
||||
_updatedSubscription = updatedSubscription;
|
||||
}
|
||||
|
||||
protected override List<string> PlanIds => new()
|
||||
{
|
||||
GetPasswordManagerPlanId(_updatedSubscription.Plan),
|
||||
_updatedSubscription.Plan.SecretsManager.StripeSeatPlanId,
|
||||
_updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
|
||||
_updatedSubscription.Plan.PasswordManager.StripeStoragePlanId
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Generates the <see cref="SubscriptionItemOptions"/> necessary to revert an <see cref="Organization"/>'s
|
||||
/// <see cref="Subscription"/> upgrade in the case of an error.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||
{
|
||||
var subscriptionItemOptions = new List<SubscriptionItemOptions>
|
||||
{
|
||||
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.
|
||||
*/
|
||||
/// <summary>
|
||||
/// Checks whether the updates provided in the <see cref="CompleteSubscriptionUpdate"/>'s constructor
|
||||
/// are actually different than the organization's current <see cref="Subscription"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the <see cref="SubscriptionItemOptions"/> necessary to upgrade an <see cref="Organization"/>'s
|
||||
/// <see cref="Subscription"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||
{
|
||||
var subscriptionItemOptions = new List<SubscriptionItemOptions>
|
||||
{
|
||||
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;
|
||||
}
|
@ -9,7 +9,7 @@ public abstract class SubscriptionUpdate
|
||||
public abstract List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription);
|
||||
public abstract List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription);
|
||||
|
||||
public bool UpdateNeeded(Subscription subscription)
|
||||
public virtual bool UpdateNeeded(Subscription subscription)
|
||||
{
|
||||
var upgradeItemsOptions = UpgradeItemsOptions(subscription);
|
||||
foreach (var upgradeItemOptions in upgradeItemsOptions)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -64,6 +64,7 @@ public record TeamsStarterPlan : Plan
|
||||
HasAdditionalStorageOption = true;
|
||||
|
||||
StripePlanId = "teams-org-starter";
|
||||
StripeStoragePlanId = "storage-gb-monthly";
|
||||
AdditionalStoragePricePerGb = 0.5M;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -18,6 +18,15 @@ public interface IPaymentService
|
||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||
short additionalStorageGb, TaxInfo taxInfo);
|
||||
Task<string> AdjustSubscription(
|
||||
Organization organization,
|
||||
Plan updatedPlan,
|
||||
int newlyPurchasedPasswordManagerSeats,
|
||||
bool subscribedToSecretsManager,
|
||||
int? newlyPurchasedSecretsManagerSeats,
|
||||
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||
int newlyPurchasedAdditionalStorage,
|
||||
DateTime? prorationDate = null);
|
||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||
Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
||||
|
@ -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<string> 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<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
|
||||
{
|
||||
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
||||
|
@ -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<Organization>(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<Organization>(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)
|
||||
|
@ -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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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<SubscriptionItem>
|
||||
{
|
||||
Data = new List<SubscriptionItem>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => 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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
upgrade.UseSecretsManager = true;
|
||||
upgrade.AdditionalSmSeats = 10;
|
||||
upgrade.AdditionalServiceAccounts = 10;
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => 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<IOrganizationService>().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<UpgradeOrganizationPlanCommand> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IOrganizationRepository>().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<IPaymentService>().Received(1).AdjustSubscription(
|
||||
organization,
|
||||
StaticStore.GetPlan(planType),
|
||||
organizationUpgrade.AdditionalSeats,
|
||||
organizationUpgrade.UseSecretsManager,
|
||||
organizationUpgrade.AdditionalSmSeats,
|
||||
5,
|
||||
3);
|
||||
await sutProvider.GetDependency<IOrganizationService>().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)]
|
||||
|
Loading…
Reference in New Issue
Block a user