1
0
mirror of https://github.com/bitwarden/server synced 2025-12-10 13:23:27 +00:00

remove hardcoded storage values (#6571)

This commit is contained in:
Kyle Denney
2025-11-17 11:16:02 -06:00
committed by GitHub
parent c620ec2aca
commit a9bb01031a
14 changed files with 85 additions and 23 deletions

View File

@@ -75,8 +75,7 @@ public class CloudOrganizationSignUpCommand(
PlanType = plan!.Type, PlanType = plan!.Type,
Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats), Seats = (short)(plan.PasswordManager.BaseSeats + signup.AdditionalSeats),
MaxCollections = plan.PasswordManager.MaxCollections, MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = !plan.PasswordManager.BaseStorageGb.HasValue ? MaxStorageGb = (short)(plan.PasswordManager.BaseStorageGb + signup.AdditionalStorageGb),
(short?)null : (short)(plan.PasswordManager.BaseStorageGb.Value + signup.AdditionalStorageGb),
UsePolicies = plan.HasPolicies, UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso, UseSso = plan.HasSso,
UseGroups = plan.HasGroups, UseGroups = plan.HasGroups,

View File

@@ -73,7 +73,7 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
PlanType = plan!.Type, PlanType = plan!.Type,
Seats = signup.AdditionalSeats, Seats = signup.AdditionalSeats,
MaxCollections = plan.PasswordManager.MaxCollections, MaxCollections = plan.PasswordManager.MaxCollections,
MaxStorageGb = 1, MaxStorageGb = plan.PasswordManager.BaseStorageGb,
UsePolicies = plan.HasPolicies, UsePolicies = plan.HasPolicies,
UseSso = plan.HasSso, UseSso = plan.HasSso,
UseOrganizationDomains = plan.HasOrganizationDomains, UseOrganizationDomains = plan.HasOrganizationDomains,

View File

@@ -148,7 +148,7 @@ public class OrganizationService : IOrganizationService
} }
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb, var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, organization, storageAdjustmentGb,
plan.PasswordManager.StripeStoragePlanId); plan.PasswordManager.StripeStoragePlanId, plan.PasswordManager.BaseStorageGb);
await ReplaceAndUpdateCacheAsync(organization); await ReplaceAndUpdateCacheAsync(organization);
return secret; return secret;
} }

View File

@@ -97,7 +97,7 @@ public abstract record Plan
public decimal PremiumAccessOptionPrice { get; init; } public decimal PremiumAccessOptionPrice { get; init; }
public short? MaxSeats { get; init; } public short? MaxSeats { get; init; }
// Storage // Storage
public short? BaseStorageGb { get; init; } public short BaseStorageGb { get; init; }
public bool HasAdditionalStorageOption { get; init; } public bool HasAdditionalStorageOption { get; init; }
public decimal AdditionalStoragePricePerGb { get; init; } public decimal AdditionalStoragePricePerGb { get; init; }
public string StripeStoragePlanId { get; init; } public string StripeStoragePlanId { get; init; }

View File

@@ -80,6 +80,8 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
return new BadRequest("Additional storage must be greater than 0."); return new BadRequest("Additional storage must be greater than 0.");
} }
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
Customer? customer; Customer? customer;
/* /*
@@ -107,7 +109,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
customer = await ReconcileBillingLocationAsync(customer, billingAddress); customer = await ReconcileBillingLocationAsync(customer, billingAddress);
var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null); var subscription = await CreateSubscriptionAsync(user.Id, customer, premiumPlan, additionalStorageGb > 0 ? additionalStorageGb : null);
paymentMethod.Switch( paymentMethod.Switch(
tokenized => tokenized =>
@@ -140,7 +142,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
user.Gateway = GatewayType.Stripe; user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id; user.GatewaySubscriptionId = subscription.Id;
user.MaxStorageGb = (short)(1 + additionalStorageGb); user.MaxStorageGb = (short)(premiumPlan.Storage.Provided + additionalStorageGb);
user.LicenseKey = CoreHelpers.SecureRandomString(20); user.LicenseKey = CoreHelpers.SecureRandomString(20);
user.RevisionDate = DateTime.UtcNow; user.RevisionDate = DateTime.UtcNow;
@@ -304,9 +306,9 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
private async Task<Subscription> CreateSubscriptionAsync( private async Task<Subscription> CreateSubscriptionAsync(
Guid userId, Guid userId,
Customer customer, Customer customer,
Pricing.Premium.Plan premiumPlan,
int? storage) int? storage)
{ {
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions> var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{ {

View File

@@ -99,7 +99,7 @@ public record PlanAdapter : Core.Models.StaticStore.Plan
_ => true); _ => true);
var baseSeats = GetBaseSeats(plan.Seats); var baseSeats = GetBaseSeats(plan.Seats);
var maxSeats = GetMaxSeats(plan.Seats); var maxSeats = GetMaxSeats(plan.Seats);
var baseStorageGb = (short?)plan.Storage?.Provided; var baseStorageGb = (short)(plan.Storage?.Provided ?? 0);
var hasAdditionalStorageOption = plan.Storage != null; var hasAdditionalStorageOption = plan.Storage != null;
var additionalStoragePricePerGb = plan.Storage?.Price ?? 0; var additionalStoragePricePerGb = plan.Storage?.Price ?? 0;
var stripeStoragePlanId = plan.Storage?.StripePriceId; var stripeStoragePlanId = plan.Storage?.StripePriceId;

View File

@@ -4,4 +4,5 @@ public class Purchasable
{ {
public string StripePriceId { get; init; } = null!; public string StripePriceId { get; init; } = null!;
public decimal Price { get; init; } public decimal Price { get; init; }
public int Provided { get; init; }
} }

View File

@@ -186,6 +186,6 @@ public class PricingClient(
Available = true, Available = true,
LegacyYear = null, LegacyYear = null,
Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, Seat = new Purchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal } Storage = new Purchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
}; };
} }

View File

@@ -101,7 +101,9 @@ public class PremiumUserBillingService(
*/ */
customer = await ReconcileBillingLocationAsync(customer, customerSetup.TaxInformation); customer = await ReconcileBillingLocationAsync(customer, customerSetup.TaxInformation);
var subscription = await CreateSubscriptionAsync(user.Id, customer, storage); var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscription = await CreateSubscriptionAsync(user.Id, customer, premiumPlan, storage);
switch (customerSetup.TokenizedPaymentSource) switch (customerSetup.TokenizedPaymentSource)
{ {
@@ -119,6 +121,7 @@ public class PremiumUserBillingService(
user.Gateway = GatewayType.Stripe; user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id; user.GatewayCustomerId = customer.Id;
user.GatewaySubscriptionId = subscription.Id; user.GatewaySubscriptionId = subscription.Id;
user.MaxStorageGb = (short)(premiumPlan.Storage.Provided + (storage ?? 0));
await userRepository.ReplaceAsync(user); await userRepository.ReplaceAsync(user);
} }
@@ -301,9 +304,9 @@ public class PremiumUserBillingService(
private async Task<Subscription> CreateSubscriptionAsync( private async Task<Subscription> CreateSubscriptionAsync(
Guid userId, Guid userId,
Customer customer, Customer customer,
Pricing.Premium.Plan premiumPlan,
int? storage) int? storage)
{ {
var premiumPlan = await pricingClient.GetAvailablePremiumPlan();
var subscriptionItemOptionsList = new List<SubscriptionItemOptions> var subscriptionItemOptionsList = new List<SubscriptionItemOptions>
{ {

View File

@@ -299,7 +299,7 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount ? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount
: 0, : 0,
PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue
? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) : ? organization.MaxStorageGb.Value - plan.PasswordManager.BaseStorageGb :
0 0
}; };
} }

View File

@@ -254,9 +254,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
organization.UseApi = newPlan.HasApi; organization.UseApi = newPlan.HasApi;
organization.SelfHost = newPlan.HasSelfHost; organization.SelfHost = newPlan.HasSelfHost;
organization.UsePolicies = newPlan.HasPolicies; organization.UsePolicies = newPlan.HasPolicies;
organization.MaxStorageGb = !newPlan.PasswordManager.BaseStorageGb.HasValue organization.MaxStorageGb = (short)(newPlan.PasswordManager.BaseStorageGb + upgrade.AdditionalStorageGb);
? (short?)null
: (short)(newPlan.PasswordManager.BaseStorageGb.Value + upgrade.AdditionalStorageGb);
organization.UseGroups = newPlan.HasGroups; organization.UseGroups = newPlan.HasGroups;
organization.UseDirectory = newPlan.HasDirectory; organization.UseDirectory = newPlan.HasDirectory;
organization.UseEvents = newPlan.HasEvents; organization.UseEvents = newPlan.HasEvents;

View File

@@ -904,7 +904,6 @@ public class UserService : UserManager<User>, IUserService
} }
else else
{ {
user.MaxStorageGb = (short)(1 + additionalStorageGb);
user.LicenseKey = CoreHelpers.SecureRandomString(20); user.LicenseKey = CoreHelpers.SecureRandomString(20);
} }
@@ -977,7 +976,8 @@ public class UserService : UserManager<User>, IUserService
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan(); var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId); var baseStorageGb = (short)premiumPlan.Storage.Provided;
var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, premiumPlan.Storage.StripePriceId, baseStorageGb);
await SaveUserAsync(user); await SaveUserAsync(user);
return secret; return secret;
} }

View File

@@ -7,7 +7,7 @@ namespace Bit.Core.Utilities;
public static class BillingHelpers public static class BillingHelpers
{ {
internal static async Task<string> AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber, internal static async Task<string> AdjustStorageAsync(IPaymentService paymentService, IStorableSubscriber storableSubscriber,
short storageAdjustmentGb, string storagePlanId) short storageAdjustmentGb, string storagePlanId, short baseStorageGb)
{ {
if (storableSubscriber == null) if (storableSubscriber == null)
{ {
@@ -30,9 +30,9 @@ public static class BillingHelpers
} }
var newStorageGb = (short)(storableSubscriber.MaxStorageGb.Value + storageAdjustmentGb); var newStorageGb = (short)(storableSubscriber.MaxStorageGb.Value + storageAdjustmentGb);
if (newStorageGb < 1) if (newStorageGb < baseStorageGb)
{ {
newStorageGb = 1; newStorageGb = baseStorageGb;
} }
if (newStorageGb > 100) if (newStorageGb > 100)
@@ -48,7 +48,7 @@ public static class BillingHelpers
"Delete some stored data first."); "Delete some stored data first.");
} }
var additionalStorage = newStorageGb - 1; var additionalStorage = newStorageGb - baseStorageGb;
var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber, var paymentIntentClientSecret = await paymentService.AdjustStorageAsync(storableSubscriber,
additionalStorage, storagePlanId); additionalStorage, storagePlanId);
storableSubscriber.MaxStorageGb = newStorageGb; storableSubscriber.MaxStorageGb = newStorageGb;

View File

@@ -53,7 +53,7 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
Available = true, Available = true,
LegacyYear = null, LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually }, Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal } Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
}; };
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan); _pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
@@ -720,4 +720,63 @@ public class CreatePremiumCloudHostedSubscriptionCommandTests
await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()); await _stripeAdapter.DidNotReceive().SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>());
await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>()); await _userService.DidNotReceive().SaveUserAsync(Arg.Any<User>());
} }
[Theory, BitAutoData]
public async Task Run_WithAdditionalStorage_SetsCorrectMaxStorageGb(
User user,
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
// Arrange
user.Premium = false;
user.GatewayCustomerId = null;
user.Email = "test@example.com";
paymentMethod.Type = TokenizablePaymentMethodType.Card;
paymentMethod.Token = "card_token_123";
billingAddress.Country = "US";
billingAddress.PostalCode = "12345";
const short additionalStorage = 2;
// Setup premium plan with 5GB provided storage
var premiumPlan = new PremiumPlan
{
Name = "Premium",
Available = true,
LegacyYear = null,
Seat = new PremiumPurchasable { Price = 10M, StripePriceId = StripeConstants.Prices.PremiumAnnually },
Storage = new PremiumPurchasable { Price = 4M, StripePriceId = StripeConstants.Prices.StoragePlanPersonal, Provided = 1 }
};
_pricingClient.GetAvailablePremiumPlan().Returns(premiumPlan);
var mockCustomer = Substitute.For<StripeCustomer>();
mockCustomer.Id = "cust_123";
mockCustomer.Address = new Address { Country = "US", PostalCode = "12345" };
mockCustomer.Metadata = new Dictionary<string, string>();
var mockSubscription = Substitute.For<StripeSubscription>();
mockSubscription.Id = "sub_123";
mockSubscription.Status = "active";
mockSubscription.Items = new StripeList<SubscriptionItem>
{
Data =
[
new SubscriptionItem
{
CurrentPeriodEnd = DateTime.UtcNow.AddDays(30)
}
]
};
_stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>()).Returns(mockCustomer);
_stripeAdapter.SubscriptionCreateAsync(Arg.Any<SubscriptionCreateOptions>()).Returns(mockSubscription);
// Act
var result = await _command.Run(user, paymentMethod, billingAddress, additionalStorage);
// Assert
Assert.True(result.IsT0);
Assert.Equal((short)3, user.MaxStorageGb); // 1 (provided) + 2 (additional) = 3
await _userService.Received(1).SaveUserAsync(user);
}
} }