1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 08:32:50 -05:00

[AC-1708] Teams Starter Plan (#3386)

* Upgraded old 2019 plans to have the same features as 2020 and beyond

* Removed redundant test and moved additional test cases to GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull

* Fixed issue where feature flag wasn't returning correct plans

* Added teams 2010 plan

* Reverted accidental change to StripePaymentService

* Split feature flag logic and added some explanatory comments

* Removed families changes

* Resolved issue where Teams Starter could not sign up for a new org with SM enabled

* Fixed issue with signing up for SM with Teams Starter

* Resolved issue where an active plan could increase their SM seat count to be greater than the base seats in the password manager plan

* Updated unit test to ensure Seats are higher than SmSeats

* Resolved issue where getting plans would return a value that LINQ previously cached when feature flag was in a different state
This commit is contained in:
Conner Turnbull
2023-11-03 18:26:47 -04:00
committed by GitHub
parent 92ffe5fc20
commit 3eb4d547a8
18 changed files with 171 additions and 8 deletions

View File

@ -82,7 +82,7 @@
<label asp-for="PlanType"></label>
@{
var planTypes = Enum.GetValues<PlanType>()
.Where(p => Model.Provider == null || p is >= PlanType.TeamsMonthly and <= PlanType.EnterpriseAnnually)
.Where(p => Model.Provider == null || p is >= PlanType.TeamsMonthly and <= PlanType.TeamsStarter)
.Select(e => new SelectListItem
{
Value = ((int)e).ToString(),

View File

@ -74,6 +74,7 @@
case '@((byte)PlanType.TeamsAnnually2020)':
case '@((byte)PlanType.TeamsMonthly)':
case '@((byte)PlanType.TeamsAnnually)':
case '@((byte)PlanType.TeamsStarter)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
document.getElementById('@(nameof(Model.UseSso))').checked = false;
document.getElementById('@(nameof(Model.UseGroups))').checked = true;

View File

@ -33,9 +33,12 @@ public class PlansController : Controller
public ListResponseModel<PlanResponseModel> Get()
{
var plansUpgradeIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.BillingPlansUpgrade, _currentContext);
var data = StaticStore.Plans;
var responses = data
.Where(plan => plansUpgradeIsEnabled || plan.Type <= PlanType.EnterpriseAnnually2020)
var teamsStarterPlanIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.BillingStarterPlan, _currentContext);
var responses = StaticStore.Plans
// If plans upgrade is disabled, return only the original plans. Otherwise, return everything
.Where(plan => plansUpgradeIsEnabled || plan.Type <= PlanType.EnterpriseAnnually2020 || plan.Type == PlanType.TeamsStarter)
// If teams starter is disabled, don't return that plan, otherwise return everything
.Where(plan => teamsStarterPlanIsEnabled || plan.Product != ProductType.TeamsStarter)
.Select(plan =>
{
if (!plansUpgradeIsEnabled && plan.Type is <= PlanType.EnterpriseAnnually2020 and >= PlanType.TeamsMonthly2020)

View File

@ -164,6 +164,7 @@ public class FreshsalesController : Controller
case PlanType.TeamsMonthly:
case PlanType.TeamsMonthly2020:
case PlanType.TeamsMonthly2019:
case PlanType.TeamsStarter:
planName = "Teams";
return true;
case PlanType.EnterpriseAnnually:

View File

@ -64,6 +64,7 @@ public static class FeatureFlagKeys
public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share";
public const string BillingPlansUpgrade = "billing-plans-upgrade";
public const string BillingStarterPlan = "billing-starter-plan";
public static List<string> GetAllKeys()
{

View File

@ -36,4 +36,6 @@ public enum PlanType : byte
EnterpriseMonthly = 14,
[Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually = 15,
[Display(Name = "Teams Starter")]
TeamsStarter = 16,
}

View File

@ -12,5 +12,7 @@ public enum ProductType : byte
Teams = 2,
[Display(Name = "Enterprise")]
Enterprise = 3,
[Display(Name = "Teams Starter")]
TeamsStarter = 4,
}

View File

@ -0,0 +1,70 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record TeamsStarterPlan : Plan
{
public TeamsStarterPlan()
{
Type = PlanType.TeamsStarter;
Product = ProductType.TeamsStarter;
Name = "Teams (Starter)";
NameLocalizationKey = "planNameTeamsStarter";
DescriptionLocalizationKey = "planDescTeams";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
UsersGetPremium = true;
UpgradeSortOrder = 2;
DisplaySortOrder = 2;
PasswordManager = new TeamsStarterPasswordManagerFeatures();
SecretsManager = new TeamsStarterSecretsManagerFeatures();
}
private record TeamsStarterSecretsManagerFeatures : SecretsManagerPlanFeatures
{
public TeamsStarterSecretsManagerFeatures()
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
}
}
private record TeamsStarterPasswordManagerFeatures : PasswordManagerPlanFeatures
{
public TeamsStarterPasswordManagerFeatures()
{
BaseSeats = 10;
BaseStorageGb = 1;
BasePrice = 20;
MaxSeats = 10;
HasAdditionalStorageOption = true;
StripePlanId = "teams-org-starter";
AdditionalStoragePricePerGb = 0.5M;
}
}
}

View File

@ -243,6 +243,12 @@ public class UpdateSecretsManagerSubscriptionCommand : IUpdateSecretsManagerSubs
"You cannot decrease your subscription below your current occupied seat count.");
}
}
// Check that SM seats aren't greater than password manager seats
if (organization.Seats < update.SmSeats.Value)
{
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
}
}
private async Task ValidateSmServiceAccountsUpdateAsync(SecretsManagerSubscriptionUpdate update)

View File

@ -1893,7 +1893,10 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Plan does not allow additional Service Accounts.");
}
if (upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats)
if ((plan.Product == ProductType.TeamsStarter &&
upgrade.AdditionalSmSeats.GetValueOrDefault() > plan.PasswordManager.BaseSeats) ||
(plan.Product != ProductType.TeamsStarter &&
upgrade.AdditionalSmSeats.GetValueOrDefault() > upgrade.AdditionalSeats))
{
throw new BadRequestException("You cannot have more Secrets Manager seats than Password Manager seats.");
}

View File

@ -106,10 +106,11 @@ public static class StaticStore
GlobalDomains.Add(GlobalEquivalentDomainsType.Pinterest, new List<string> { "pinterest.com", "pinterest.com.au", "pinterest.cl", "pinterest.de", "pinterest.dk", "pinterest.es", "pinterest.fr", "pinterest.co.uk", "pinterest.jp", "pinterest.co.kr", "pinterest.nz", "pinterest.pt", "pinterest.se" });
#endregion
Plans = new List<Models.StaticStore.Plan>
Plans = new List<Plan>
{
new EnterprisePlan(true),
new EnterprisePlan(false),
new TeamsStarterPlan(),
new TeamsPlan(true),
new TeamsPlan(false),
@ -130,7 +131,7 @@ public static class StaticStore
}
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
public static IEnumerable<Models.StaticStore.Plan> Plans { get; }
public static IEnumerable<Plan> Plans { get; }
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
{
new SponsoredPlan