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:
@ -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; }
|
||||
}
|
||||
|
56
src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs
Normal file
56
src/Core/Models/Business/SecretsManagerSubscriptionUpdate.cs
Normal 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; }
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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; }
|
||||
|
@ -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;
|
||||
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail;
|
||||
|
||||
public class OrganizationServiceAccountsMaxReachedViewModel
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public int MaxServiceAccountsCount { get; set; }
|
||||
}
|
@ -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; }
|
||||
|
Reference in New Issue
Block a user