1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

[AC-1486] Feature: SM Billing (#3073)

* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem (#3037)

* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem

* [AC-1423] Add helper to StaticStore.cs to find a Plan by StripePlanId

* [AC-1423] Use the helper method to set SubscriptionInfo.BitwardenProduct

* Add SecretsManagerBilling feature flag to Constants

* [AC 1409] Secrets Manager Subscription Stripe Integration  (#3019)

* [AC-1418] Add missing SecretsManagerPlan property to OrganizationResponseModel (#3055)

* [AC 1460] Update Stripe Configuration (#3070)

* [AC 1410] Secrets Manager subscription adjustment back-end changes (#3036)

* Create UpgradeSecretsManagerSubscription command

* [AC-1495] Extract UpgradePlanAsync into a command (#3081)

* This is a pure lift & shift with no refactors

* [AC-1503] Fix Stripe integration on organization upgrade (#3084)

* Fix SM parameters not being passed to Stripe

* [AC-1504] Allow SM max autoscale limits to be disabled (#3085)

* [AC-1488] Changed SM Signup and Upgrade paths to set SmServiceAccounts to include the plan BaseServiceAccount (#3086)

* [AC-1510] Enable access to Secrets Manager to Organization owner for new Subscription (#3089)

* Revert changes to ReferenceEvent code (#3091)

This will be done in AC-1481

* Add UsePasswordManager to sync data (#3114)

* [AC-1522] Fix service account check on upgrading (#3111)

* [AC-1521] Address checkmarx security feedback (#3124)

* Reinstate target attribute but add noopener noreferrer

* Update date on migration script

---------

Co-authored-by: Shane Melton <smelton@bitwarden.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: cyprain-okeke <cokeke@bitwarden.com>
Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com>
Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
Co-authored-by: Rui Tome <rtome@bitwarden.com>
This commit is contained in:
Álison Fernandes
2023-07-24 23:05:05 +01:00
committed by GitHub
parent 4ec765ae19
commit 35111382e5
50 changed files with 2880 additions and 381 deletions

View File

@ -12,4 +12,7 @@ public class OrganizationUpgrade
public TaxInfo TaxInfo { get; set; }
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
public int? AdditionalSmSeats { get; set; }
public int? AdditionalServiceAccounts { get; set; }
public bool UseSecretsManager { get; set; }
}

View File

@ -0,0 +1,56 @@
namespace Bit.Core.Models.Business;
public class SecretsManagerSubscriptionUpdate
{
public Guid OrganizationId { get; set; }
/// <summary>
/// The seats to be added or removed from the organization
/// </summary>
public int SmSeatsAdjustment { get; set; }
/// <summary>
/// The total seats the organization will have after the update, including any base seats included in the plan
/// </summary>
public int SmSeats { get; set; }
/// <summary>
/// The seats the organization will have after the update, excluding the base seats included in the plan
/// Usually this is what the organization is billed for
/// </summary>
public int SmSeatsExcludingBase { get; set; }
/// <summary>
/// The new autoscale limit for seats, expressed as a total (not an adjustment).
/// This may or may not be the same as the current autoscale limit.
/// </summary>
public int? MaxAutoscaleSmSeats { get; set; }
/// <summary>
/// The service accounts to be added or removed from the organization
/// </summary>
public int SmServiceAccountsAdjustment { get; set; }
/// <summary>
/// The total service accounts the organization will have after the update, including the base service accounts
/// included in the plan
/// </summary>
public int SmServiceAccounts { get; set; }
/// <summary>
/// The seats the organization will have after the update, excluding the base seats included in the plan
/// Usually this is what the organization is billed for
/// </summary>
public int SmServiceAccountsExcludingBase { get; set; }
/// <summary>
/// The new autoscale limit for service accounts, expressed as a total (not an adjustment).
/// This may or may not be the same as the current autoscale limit.
/// </summary>
public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool SmSeatsChanged => SmSeatsAdjustment != 0;
public bool SmServiceAccountsChanged => SmServiceAccountsAdjustment != 0;
public bool MaxAutoscaleSmSeatsChanged { get; set; }
public bool MaxAutoscaleSmServiceAccountsChanged { get; set; }
}

View File

@ -1,36 +1,61 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Stripe;
namespace Bit.Core.Models.Business;
public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions
{
public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon)
public OrganizationSubscriptionOptionsBase(Organization org, List<StaticStore.Plan> plans, TaxInfo taxInfo, int additionalSeats,
int additionalStorageGb, bool premiumAccessAddon, int additionalSmSeats, int additionalServiceAccounts)
{
Items = new List<SubscriptionItemOptions>();
Metadata = new Dictionary<string, string>
{
[org.GatewayIdField()] = org.Id.ToString()
};
foreach (var plan in plans)
{
AddPlanIdToSubscription(plan);
if (plan.StripePlanId != null)
switch (plan.BitwardenProduct)
{
case BitwardenProductType.PasswordManager:
{
AddPremiumAccessAddon(premiumAccessAddon, plan);
AddAdditionalSeatToSubscription(additionalSeats, plan);
AddAdditionalStorage(additionalStorageGb, plan);
break;
}
case BitwardenProductType.SecretsManager:
{
AddAdditionalSeatToSubscription(additionalSmSeats, plan);
AddServiceAccount(additionalServiceAccounts, plan);
break;
}
}
}
if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
{
DefaultTaxRates = new List<string> { taxInfo.StripeTaxRateId };
}
}
private void AddServiceAccount(int additionalServiceAccounts, StaticStore.Plan plan)
{
if (additionalServiceAccounts > 0 && plan.StripeServiceAccountPlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripePlanId,
Quantity = 1
});
}
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripeSeatPlanId,
Quantity = additionalSeats
Plan = plan.StripeServiceAccountPlanId,
Quantity = additionalServiceAccounts
});
}
}
private void AddAdditionalStorage(int additionalStorageGb, StaticStore.Plan plan)
{
if (additionalStorageGb > 0)
{
Items.Add(new SubscriptionItemOptions
@ -39,19 +64,29 @@ public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOpti
Quantity = additionalStorageGb
});
}
}
private void AddPremiumAccessAddon(bool premiumAccessAddon, StaticStore.Plan plan)
{
if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
{
Items.Add(new SubscriptionItemOptions
{
Plan = plan.StripePremiumAccessPlanId,
Quantity = 1
});
Items.Add(new SubscriptionItemOptions { Plan = plan.StripePremiumAccessPlanId, Quantity = 1 });
}
}
if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId))
private void AddAdditionalSeatToSubscription(int additionalSeats, StaticStore.Plan plan)
{
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
{
DefaultTaxRates = new List<string> { taxInfo.StripeTaxRateId };
Items.Add(new SubscriptionItemOptions { Plan = plan.StripeSeatPlanId, Quantity = additionalSeats });
}
}
private void AddPlanIdToSubscription(StaticStore.Plan plan)
{
if (plan.StripePlanId != null)
{
Items.Add(new SubscriptionItemOptions { Plan = plan.StripePlanId, Quantity = 1 });
}
}
}
@ -59,13 +94,14 @@ public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOpti
public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase
{
public OrganizationPurchaseSubscriptionOptions(
Organization org, StaticStore.Plan plan,
TaxInfo taxInfo, int additionalSeats = 0,
int additionalStorageGb = 0, bool premiumAccessAddon = false) :
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
Organization org, List<StaticStore.Plan> plans,
TaxInfo taxInfo, int additionalSeats,
int additionalStorageGb, bool premiumAccessAddon,
int additionalSmSeats, int additionalServiceAccounts) :
base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccounts)
{
OffSession = true;
TrialPeriodDays = plan.TrialPeriodDays;
TrialPeriodDays = plans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.PasswordManager)!.TrialPeriodDays;
}
}
@ -73,10 +109,10 @@ public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOp
{
public OrganizationUpgradeSubscriptionOptions(
string customerId, Organization org,
StaticStore.Plan plan, TaxInfo taxInfo,
int additionalSeats = 0, int additionalStorageGb = 0,
bool premiumAccessAddon = false) :
base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon)
List<StaticStore.Plan> plans, OrganizationUpgrade upgrade) :
base(org, plans, upgrade.TaxInfo, upgrade.AdditionalSeats, upgrade.AdditionalStorageGb,
upgrade.PremiumAccessAddon, upgrade.AdditionalSmSeats.GetValueOrDefault(),
upgrade.AdditionalServiceAccounts.GetValueOrDefault())
{
Customer = customerId;
}

View File

@ -1,4 +1,5 @@
using Stripe;
using Bit.Core.Enums;
using Stripe;
namespace Bit.Core.Models.Business;
@ -46,12 +47,20 @@ public class SubscriptionInfo
Name = item.Plan.Nickname;
Amount = item.Plan.Amount.GetValueOrDefault() / 100M;
Interval = item.Plan.Interval;
AddonSubscriptionItem =
Utilities.StaticStore.IsAddonSubscriptionItem(item.Plan.Id);
BitwardenProduct =
Utilities.StaticStore.GetPlanByStripeId(item.Plan.Id)?.BitwardenProduct ?? BitwardenProductType.PasswordManager;
}
Quantity = (int)item.Quantity;
SponsoredSubscriptionItem = Utilities.StaticStore.SponsoredPlans.Any(p => p.StripePlanId == item.Plan.Id);
}
public BitwardenProductType BitwardenProduct { get; set; }
public bool AddonSubscriptionItem { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public int Quantity { get; set; }

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Stripe;
namespace Bit.Core.Models.Business;
@ -42,7 +43,15 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
{
_plan = plan;
_additionalSeats = additionalSeats;
_previousSeats = organization.Seats ?? 0;
switch (plan.BitwardenProduct)
{
case BitwardenProductType.PasswordManager:
_previousSeats = organization.Seats.GetValueOrDefault();
break;
case BitwardenProductType.SecretsManager:
_previousSeats = organization.SmSeats.GetValueOrDefault();
break;
}
}
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
@ -77,6 +86,52 @@ public class SeatSubscriptionUpdate : SubscriptionUpdate
}
}
public class ServiceAccountSubscriptionUpdate : SubscriptionUpdate
{
private long? _prevServiceAccounts;
private readonly StaticStore.Plan _plan;
private readonly long? _additionalServiceAccounts;
protected override List<string> PlanIds => new() { _plan.StripeServiceAccountPlanId };
public ServiceAccountSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalServiceAccounts)
{
_plan = plan;
_additionalServiceAccounts = additionalServiceAccounts;
_prevServiceAccounts = organization.SmServiceAccounts ?? 0;
}
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
{
var item = SubscriptionItem(subscription, PlanIds.Single());
_prevServiceAccounts = item?.Quantity ?? 0;
return new()
{
new SubscriptionItemOptions
{
Id = item?.Id,
Plan = PlanIds.Single(),
Quantity = _additionalServiceAccounts,
Deleted = (item?.Id != null && _additionalServiceAccounts == 0) ? true : (bool?)null,
}
};
}
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
{
var item = SubscriptionItem(subscription, PlanIds.Single());
return new()
{
new SubscriptionItemOptions
{
Id = item?.Id,
Plan = PlanIds.Single(),
Quantity = _prevServiceAccounts,
Deleted = _prevServiceAccounts == 0 ? true : (bool?)null,
}
};
}
}
public class StorageSubscriptionUpdate : SubscriptionUpdate
{
private long? _prevStorage;

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Models.Mail;
public class OrganizationServiceAccountsMaxReachedViewModel
{
public Guid OrganizationId { get; set; }
public int MaxServiceAccountsCount { get; set; }
}

View File

@ -15,8 +15,11 @@ public class Plan
public short? BaseStorageGb { get; set; }
public short? MaxCollections { get; set; }
public short? MaxUsers { get; set; }
public short? MaxServiceAccounts { get; set; }
public bool AllowSeatAutoscale { get; set; }
public bool AllowServiceAccountsAutoscale { get; set; }
public bool HasAdditionalSeatsOption { get; set; }
public int? MaxAdditionalSeats { get; set; }
public bool HasAdditionalStorageOption { get; set; }
@ -55,7 +58,7 @@ public class Plan
public decimal PremiumAccessOptionPrice { get; set; }
public decimal? AdditionalPricePerServiceAccount { get; set; }
public short? BaseServiceAccount { get; set; }
public short? MaxServiceAccount { get; set; }
public short? MaxAdditionalServiceAccount { get; set; }
public bool HasAdditionalServiceAccountOption { get; set; }
public short? MaxProjects { get; set; }
public BitwardenProductType BitwardenProduct { get; set; }