mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[PM-16684] Integrate Pricing Service behind FF (#5276)
* Remove gRPC and convert PricingClient to HttpClient wrapper * Add PlanType.GetProductTier extension Many instances of StaticStore use are just to get the ProductTierType of a PlanType, but this can be derived from the PlanType itself without having to fetch the entire plan. * Remove invocations of the StaticStore in non-Test code * Deprecate StaticStore entry points * Run dotnet format * Matt's feedback * Run dotnet format * Rui's feedback * Run dotnet format * Replacements since approval * Run dotnet format
This commit is contained in:
@ -10,6 +10,7 @@ using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -56,8 +57,8 @@ public class OrganizationsController : Controller
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -84,8 +85,8 @@ public class OrganizationsController : Controller
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IProviderBillingService providerBillingService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand)
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -111,8 +112,8 @@ public class OrganizationsController : Controller
|
||||
_providerOrganizationRepository = providerOrganizationRepository;
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_providerBillingService = providerBillingService;
|
||||
_featureService = featureService;
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -212,6 +213,8 @@ public class OrganizationsController : Controller
|
||||
? await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id)
|
||||
: -1;
|
||||
|
||||
var plans = await _pricingClient.ListPlans();
|
||||
|
||||
return View(new OrganizationEditModel(
|
||||
organization,
|
||||
provider,
|
||||
@ -224,6 +227,7 @@ public class OrganizationsController : Controller
|
||||
billingHistoryInfo,
|
||||
billingSyncConnection,
|
||||
_globalSettings,
|
||||
plans,
|
||||
secrets,
|
||||
projects,
|
||||
serviceAccounts,
|
||||
@ -253,8 +257,9 @@ public class OrganizationsController : Controller
|
||||
|
||||
UpdateOrganization(organization, model);
|
||||
|
||||
if (organization.UseSecretsManager &&
|
||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
|
||||
{
|
||||
TempData["Error"] = "Plan does not support Secrets Manager";
|
||||
return RedirectToAction("Edit", new { id });
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@ -17,6 +18,8 @@ namespace Bit.Admin.AdminConsole.Models;
|
||||
|
||||
public class OrganizationEditModel : OrganizationViewModel
|
||||
{
|
||||
private readonly List<Plan> _plans;
|
||||
|
||||
public OrganizationEditModel() { }
|
||||
|
||||
public OrganizationEditModel(Provider provider)
|
||||
@ -40,6 +43,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
IEnumerable<OrganizationConnection> connections,
|
||||
GlobalSettings globalSettings,
|
||||
List<Plan> plans,
|
||||
int secrets,
|
||||
int projects,
|
||||
int serviceAccounts,
|
||||
@ -96,6 +100,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
_plans = plans;
|
||||
}
|
||||
|
||||
public BillingInfo BillingInfo { get; set; }
|
||||
@ -183,7 +189,7 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
* Add mappings for individual properties as you need them
|
||||
*/
|
||||
public object GetPlansHelper() =>
|
||||
StaticStore.Plans
|
||||
_plans
|
||||
.Select(p =>
|
||||
{
|
||||
var plan = new
|
||||
|
@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
|
||||
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
|
||||
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller
|
||||
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
|
||||
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
|
||||
_featureService = featureService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-17000
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -60,6 +61,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
|
||||
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
||||
IOrganizationDeleteCommand organizationDeleteCommand)
|
||||
IOrganizationDeleteCommand organizationDeleteCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -103,6 +106,7 @@ public class OrganizationsController : Controller
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
|
||||
_organizationDeleteCommand = organizationDeleteCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -120,7 +124,8 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new OrganizationResponseModel(organization);
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
return new OrganizationResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -181,7 +186,8 @@ public class OrganizationsController : Controller
|
||||
|
||||
var organizationSignup = model.ToOrganizationSignup(user);
|
||||
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
|
||||
return new OrganizationResponseModel(result.Organization);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
|
||||
return new OrganizationResponseModel(result.Organization, plan);
|
||||
}
|
||||
|
||||
[HttpPost("create-without-payment")]
|
||||
@ -196,7 +202,8 @@ public class OrganizationsController : Controller
|
||||
|
||||
var organizationSignup = model.ToOrganizationSignup(user);
|
||||
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
|
||||
return new OrganizationResponseModel(result.Organization);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
|
||||
return new OrganizationResponseModel(result.Organization, plan);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
@ -224,7 +231,8 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling);
|
||||
return new OrganizationResponseModel(organization);
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
return new OrganizationResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/storage")]
|
||||
@ -358,8 +366,8 @@ public class OrganizationsController : Controller
|
||||
if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)
|
||||
{
|
||||
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
|
||||
var productTier = organization.PlanType.GetProductTier();
|
||||
if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -542,7 +550,8 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
|
||||
return new OrganizationResponseModel(organization);
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
return new OrganizationResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/plan-type")]
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Utilities;
|
||||
using Constants = Bit.Core.Constants;
|
||||
|
||||
@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationResponseModel(Organization organization, string obj = "organization")
|
||||
: base(obj)
|
||||
public OrganizationResponseModel(
|
||||
Organization organization,
|
||||
Plan plan,
|
||||
string obj = "organization") : base(obj)
|
||||
{
|
||||
if (organization == null)
|
||||
{
|
||||
@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel
|
||||
BusinessCountry = organization.BusinessCountry;
|
||||
BusinessTaxNumber = organization.BusinessTaxNumber;
|
||||
BillingEmail = organization.BillingEmail;
|
||||
Plan = new PlanResponseModel(StaticStore.GetPlan(organization.PlanType));
|
||||
// Self-Host instances only require plan information that can be derived from the Organization record.
|
||||
Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization);
|
||||
PlanType = organization.PlanType;
|
||||
Seats = organization.Seats;
|
||||
MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
|
||||
@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
{
|
||||
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription")
|
||||
public OrganizationSubscriptionResponseModel(
|
||||
Organization organization,
|
||||
Plan plan) : base(organization, plan, "organizationSubscription")
|
||||
{
|
||||
Expiration = organization.ExpirationDate;
|
||||
StorageName = organization.Storage.HasValue ?
|
||||
@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData)
|
||||
: this(organization)
|
||||
public OrganizationSubscriptionResponseModel(
|
||||
Organization organization,
|
||||
SubscriptionInfo subscription,
|
||||
Plan plan,
|
||||
bool hideSensitiveData) : this(organization, plan)
|
||||
{
|
||||
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
|
||||
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
|
||||
@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
||||
}
|
||||
|
||||
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
|
||||
this(organization)
|
||||
this(organization, (Plan)null)
|
||||
{
|
||||
if (license != null)
|
||||
{
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
UsePasswordManager = organization.UsePasswordManager;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise;
|
||||
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
|
||||
SelfHost = organization.SelfHost;
|
||||
Seats = organization.Seats;
|
||||
MaxCollections = organization.MaxCollections;
|
||||
@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
|
||||
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
|
||||
.UsersCanSponsor(organization);
|
||||
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
|
||||
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
||||
|
@ -1,8 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response;
|
||||
|
||||
@ -26,7 +26,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UsersGetPremium = organization.UsersGetPremium;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise;
|
||||
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
|
||||
SelfHost = organization.SelfHost;
|
||||
Seats = organization.Seats;
|
||||
MaxCollections = organization.MaxCollections;
|
||||
@ -44,7 +44,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
ProviderId = organization.ProviderId;
|
||||
ProviderName = organization.ProviderName;
|
||||
ProviderType = organization.ProviderType;
|
||||
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
||||
ProductTierType = organization.PlanType.GetProductTier();
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
@ -21,6 +22,7 @@ public class OrganizationBillingController(
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
IPricingClient pricingClient,
|
||||
ISubscriberService subscriberService,
|
||||
IPaymentHistoryService paymentHistoryService,
|
||||
IUserService userService) : BaseBillingController
|
||||
@ -279,7 +281,7 @@ public class OrganizationBillingController(
|
||||
}
|
||||
var organizationSignup = model.ToOrganizationSignup(user);
|
||||
var sale = OrganizationSale.From(organization, organizationSignup);
|
||||
var plan = StaticStore.GetPlan(model.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
|
||||
sale.Organization.PlanType = plan.Type;
|
||||
sale.Organization.Plan = plan.Name;
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -45,7 +46,8 @@ public class OrganizationsController(
|
||||
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
|
||||
IReferenceEventService referenceEventService,
|
||||
ISubscriberService subscriberService,
|
||||
IOrganizationInstallationRepository organizationInstallationRepository)
|
||||
IOrganizationInstallationRepository organizationInstallationRepository,
|
||||
IPricingClient pricingClient)
|
||||
: Controller
|
||||
{
|
||||
[HttpGet("{id:guid}/subscription")]
|
||||
@ -62,26 +64,28 @@ public class OrganizationsController(
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && organization.Gateway != null)
|
||||
{
|
||||
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscriptionInfo == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var hideSensitiveData = !await currentContext.EditSubscription(id);
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData);
|
||||
}
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
|
||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
||||
}
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization);
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (string.IsNullOrEmpty(organization.GatewaySubscriptionId))
|
||||
{
|
||||
return new OrganizationSubscriptionResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
var subscriptionInfo = await paymentService.GetSubscriptionAsync(organization);
|
||||
if (subscriptionInfo == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var hideSensitiveData = !await currentContext.EditSubscription(id);
|
||||
|
||||
return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, plan, hideSensitiveData);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}/license")]
|
||||
@ -165,7 +169,8 @@ public class OrganizationsController(
|
||||
|
||||
organization = await AdjustOrganizationSeatsForSmTrialAsync(id, organization, model);
|
||||
|
||||
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization);
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
var organizationUpdate = model.ToSecretsManagerSubscriptionUpdate(organization, plan);
|
||||
|
||||
await updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -20,6 +21,7 @@ namespace Bit.Api.Billing.Controllers;
|
||||
public class ProviderBillingController(
|
||||
ICurrentContext currentContext,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IPricingClient pricingClient,
|
||||
IProviderBillingService providerBillingService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderRepository providerRepository,
|
||||
@ -84,13 +86,25 @@ public class ProviderBillingController(
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
|
||||
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
||||
{
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
return new ConfiguredProviderPlan(
|
||||
providerPlan.Id,
|
||||
providerPlan.ProviderId,
|
||||
plan,
|
||||
providerPlan.SeatMinimum ?? 0,
|
||||
providerPlan.PurchasedSeats ?? 0,
|
||||
providerPlan.AllocatedSeats ?? 0);
|
||||
}));
|
||||
|
||||
var taxInformation = GetTaxInformation(subscription.Customer);
|
||||
|
||||
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
|
||||
|
||||
var response = ProviderSubscriptionResponse.From(
|
||||
subscription,
|
||||
providerPlans,
|
||||
configuredProviderPlans,
|
||||
taxInformation,
|
||||
subscriptionSuspension,
|
||||
provider);
|
||||
|
@ -1,9 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Utilities;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
@ -25,26 +23,24 @@ public record ProviderSubscriptionResponse(
|
||||
|
||||
public static ProviderSubscriptionResponse From(
|
||||
Subscription subscription,
|
||||
ICollection<ProviderPlan> providerPlans,
|
||||
ICollection<ConfiguredProviderPlan> providerPlans,
|
||||
TaxInformation taxInformation,
|
||||
SubscriptionSuspension subscriptionSuspension,
|
||||
Provider provider)
|
||||
{
|
||||
var providerPlanResponses = providerPlans
|
||||
.Where(providerPlan => providerPlan.IsConfigured())
|
||||
.Select(ConfiguredProviderPlan.From)
|
||||
.Select(configuredProviderPlan =>
|
||||
.Select(providerPlan =>
|
||||
{
|
||||
var plan = StaticStore.GetPlan(configuredProviderPlan.PlanType);
|
||||
var cost = (configuredProviderPlan.SeatMinimum + configuredProviderPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
|
||||
var plan = providerPlan.Plan;
|
||||
var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice;
|
||||
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
|
||||
return new ProviderPlanResponse(
|
||||
plan.Name,
|
||||
plan.Type,
|
||||
plan.ProductTier,
|
||||
configuredProviderPlan.SeatMinimum,
|
||||
configuredProviderPlan.PurchasedSeats,
|
||||
configuredProviderPlan.AssignedSeats,
|
||||
providerPlan.SeatMinimum,
|
||||
providerPlan.PurchasedSeats,
|
||||
providerPlan.AssignedSeats,
|
||||
cost,
|
||||
cadence);
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using Bit.Api.Billing.Public.Models;
|
||||
using Bit.Api.Models.Public.Response;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Repositories;
|
||||
@ -21,19 +22,22 @@ public class OrganizationController : Controller
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||
private readonly ILogger<OrganizationController> _logger;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationController(
|
||||
IOrganizationService organizationService,
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
ILogger<OrganizationController> logger)
|
||||
ILogger<OrganizationController> logger,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_currentContext = currentContext;
|
||||
_organizationRepository = organizationRepository;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
_logger = logger;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -140,7 +144,8 @@ public class OrganizationController : Controller
|
||||
return "Organization has no access to Secrets Manager.";
|
||||
}
|
||||
|
||||
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization, plan);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate);
|
||||
|
||||
return string.Empty;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Api.Billing.Public.Models;
|
||||
|
||||
@ -93,17 +94,17 @@ public class SecretsManagerSubscriptionUpdateModel
|
||||
set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
|
||||
}
|
||||
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
|
||||
{
|
||||
var update = UpdateUpdateMaxAutoScale(organization);
|
||||
var update = UpdateUpdateMaxAutoScale(organization, plan);
|
||||
UpdateSeats(organization, update);
|
||||
UpdateServiceAccounts(organization, update);
|
||||
return update;
|
||||
}
|
||||
|
||||
private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization)
|
||||
private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization, Plan plan)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, false)
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
|
||||
MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -7,13 +7,15 @@ namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("plans")]
|
||||
[Authorize("Web")]
|
||||
public class PlansController : Controller
|
||||
public class PlansController(
|
||||
IPricingClient pricingClient) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
[AllowAnonymous]
|
||||
public ListResponseModel<PlanResponseModel> Get()
|
||||
public async Task<ListResponseModel<PlanResponseModel>> Get()
|
||||
{
|
||||
var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan));
|
||||
var plans = await pricingClient.ListPlans();
|
||||
var responses = plans.Select(plan => new PlanResponseModel(plan));
|
||||
return new ListResponseModel<PlanResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,8 @@ public class SelfHostedOrganizationLicensesController : Controller
|
||||
|
||||
var result = await _organizationService.SignUpAsync(license, user, model.Key,
|
||||
model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
|
||||
return new OrganizationResponseModel(result.Item1);
|
||||
|
||||
return new OrganizationResponseModel(result.Item1, null);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Api.Models.Request.Organizations;
|
||||
|
||||
@ -12,9 +13,9 @@ public class SecretsManagerSubscriptionUpdateRequestModel
|
||||
public int ServiceAccountAdjustment { get; set; }
|
||||
public int? MaxAutoscaleServiceAccounts { get; set; }
|
||||
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
|
||||
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization, Plan plan)
|
||||
{
|
||||
return new SecretsManagerSubscriptionUpdate(organization, false)
|
||||
return new SecretsManagerSubscriptionUpdate(organization, plan, false)
|
||||
{
|
||||
MaxAutoscaleSmSeats = MaxAutoscaleSeats,
|
||||
MaxAutoscaleSmServiceAccounts = MaxAutoscaleServiceAccounts
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
@ -44,6 +46,13 @@ public class PlanResponseModel : ResponseModel
|
||||
PasswordManager = new PasswordManagerPlanFeaturesResponseModel(plan.PasswordManager);
|
||||
}
|
||||
|
||||
public PlanResponseModel(Organization organization, string obj = "plan") : base(obj)
|
||||
{
|
||||
Type = organization.PlanType;
|
||||
ProductTier = organization.PlanType.GetProductTier();
|
||||
Name = organization.Plan;
|
||||
}
|
||||
|
||||
public PlanType Type { get; set; }
|
||||
public ProductTierType ProductTier { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -37,6 +38,7 @@ public class ServiceAccountsController : Controller
|
||||
private readonly IUpdateServiceAccountCommand _updateServiceAccountCommand;
|
||||
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
|
||||
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public ServiceAccountsController(
|
||||
ICurrentContext currentContext,
|
||||
@ -52,7 +54,8 @@ public class ServiceAccountsController : Controller
|
||||
ICreateServiceAccountCommand createServiceAccountCommand,
|
||||
IUpdateServiceAccountCommand updateServiceAccountCommand,
|
||||
IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
|
||||
IRevokeAccessTokensCommand revokeAccessTokensCommand)
|
||||
IRevokeAccessTokensCommand revokeAccessTokensCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_userService = userService;
|
||||
@ -66,6 +69,7 @@ public class ServiceAccountsController : Controller
|
||||
_updateServiceAccountCommand = updateServiceAccountCommand;
|
||||
_deleteServiceAccountsCommand = deleteServiceAccountsCommand;
|
||||
_revokeAccessTokensCommand = revokeAccessTokensCommand;
|
||||
_pricingClient = pricingClient;
|
||||
_createAccessTokenCommand = createAccessTokenCommand;
|
||||
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||
}
|
||||
@ -124,7 +128,9 @@ public class ServiceAccountsController : Controller
|
||||
if (newServiceAccountSlotsRequired > 0)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var update = new SecretsManagerSubscriptionUpdate(org, true)
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-17002
|
||||
var plan = await _pricingClient.GetPlanOrThrow(org!.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(org, plan, true)
|
||||
.AdjustServiceAccounts(newServiceAccountSlotsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@ -9,7 +10,6 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
namespace Bit.Billing.Services.Implementations;
|
||||
@ -28,6 +28,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public PaymentSucceededHandler(
|
||||
ILogger<PaymentSucceededHandler> logger,
|
||||
@ -41,7 +42,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IUserService userService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationEnableCommand organizationEnableCommand)
|
||||
IOrganizationEnableCommand organizationEnableCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_logger = logger;
|
||||
_stripeEventService = stripeEventService;
|
||||
@ -55,6 +57,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
||||
_userService = userService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_organizationEnableCommand = organizationEnableCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -96,9 +99,9 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
||||
return;
|
||||
}
|
||||
|
||||
var teamsMonthly = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||
var teamsMonthly = await _pricingClient.GetPlanOrThrow(PlanType.TeamsMonthly);
|
||||
|
||||
var enterpriseMonthly = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||
var enterpriseMonthly = await _pricingClient.GetPlanOrThrow(PlanType.EnterpriseMonthly);
|
||||
|
||||
var teamsMonthlyLineItem =
|
||||
subscription.Items.Data.FirstOrDefault(item =>
|
||||
@ -137,14 +140,21 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler
|
||||
}
|
||||
else if (organizationId.HasValue)
|
||||
{
|
||||
if (!subscription.Items.Any(i =>
|
||||
StaticStore.Plans.Any(p => p.PasswordManager.StripePlanId == i.Plan.Id)))
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (subscription.Items.All(item => plan.PasswordManager.StripePlanId != item.Plan.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
await _pushNotificationService.PushSyncOrganizationStatusAsync(organization);
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
|
@ -1,15 +1,17 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Repositories;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Billing.Services.Implementations;
|
||||
|
||||
public class ProviderEventService(
|
||||
ILogger<ProviderEventService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPricingClient pricingClient,
|
||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
@ -54,7 +56,14 @@ public class ProviderEventService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.Single(x => x.Name == client.Plan && providerPlans.Any(y => y.PlanType == x.Type));
|
||||
var organization = await organizationRepository.GetByIdAsync(client.OrganizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
var discountedPercentage = (100 - (invoice.Discount?.Coupon?.PercentOff ?? 0)) / 100;
|
||||
|
||||
@ -76,7 +85,7 @@ public class ProviderEventService(
|
||||
|
||||
foreach (var providerPlan in providerPlans.Where(x => x.PurchasedSeats is null or 0))
|
||||
{
|
||||
var plan = StaticStore.GetPlan(providerPlan.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
||||
|
||||
var clientSeats = invoiceItems
|
||||
.Where(item => item.PlanName == plan.Name)
|
||||
|
@ -2,11 +2,11 @@
|
||||
using Bit.Billing.Jobs;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Quartz;
|
||||
using Stripe;
|
||||
using Event = Stripe.Event;
|
||||
@ -27,6 +27,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationEnableCommand _organizationEnableCommand;
|
||||
private readonly IOrganizationDisableCommand _organizationDisableCommand;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public SubscriptionUpdatedHandler(
|
||||
IStripeEventService stripeEventService,
|
||||
@ -40,7 +41,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
ISchedulerFactory schedulerFactory,
|
||||
IFeatureService featureService,
|
||||
IOrganizationEnableCommand organizationEnableCommand,
|
||||
IOrganizationDisableCommand organizationDisableCommand)
|
||||
IOrganizationDisableCommand organizationDisableCommand,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
@ -54,6 +56,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
_featureService = featureService;
|
||||
_organizationEnableCommand = organizationEnableCommand;
|
||||
_organizationDisableCommand = organizationDisableCommand;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -156,7 +159,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
/// </summary>
|
||||
/// <param name="parsedEvent"></param>
|
||||
/// <param name="subscription"></param>
|
||||
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(Event parsedEvent,
|
||||
private async Task RemovePasswordManagerCouponIfRemovingSecretsManagerTrialAsync(
|
||||
Event parsedEvent,
|
||||
Subscription subscription)
|
||||
{
|
||||
if (parsedEvent.Data.PreviousAttributes?.items is null)
|
||||
@ -164,6 +168,22 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
return;
|
||||
}
|
||||
|
||||
var organization = subscription.Metadata.TryGetValue("organizationId", out var organizationId)
|
||||
? await _organizationRepository.GetByIdAsync(Guid.Parse(organizationId))
|
||||
: null;
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.SupportsSecretsManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previousSubscription = parsedEvent.Data
|
||||
.PreviousAttributes
|
||||
.ToObject<Subscription>() as Subscription;
|
||||
@ -171,17 +191,14 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
// This being false doesn't necessarily mean that the organization doesn't subscribe to Secrets Manager.
|
||||
// If there are changes to any subscription item, Stripe sends every item in the subscription, both
|
||||
// changed and unchanged.
|
||||
var previousSubscriptionHasSecretsManager = previousSubscription?.Items is not null &&
|
||||
previousSubscription.Items.Any(previousItem =>
|
||||
StaticStore.Plans.Any(p =>
|
||||
p.SecretsManager is not null &&
|
||||
p.SecretsManager.StripeSeatPlanId ==
|
||||
previousItem.Plan.Id));
|
||||
var previousSubscriptionHasSecretsManager =
|
||||
previousSubscription?.Items is not null &&
|
||||
previousSubscription.Items.Any(
|
||||
previousSubscriptionItem => previousSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
|
||||
var currentSubscriptionHasSecretsManager = subscription.Items.Any(i =>
|
||||
StaticStore.Plans.Any(p =>
|
||||
p.SecretsManager is not null &&
|
||||
p.SecretsManager.StripeSeatPlanId == i.Plan.Id));
|
||||
var currentSubscriptionHasSecretsManager =
|
||||
subscription.Items.Any(
|
||||
currentSubscriptionItem => currentSubscriptionItem.Plan.Id == plan.SecretsManager.StripeSeatPlanId);
|
||||
|
||||
if (!previousSubscriptionHasSecretsManager || currentSubscriptionHasSecretsManager)
|
||||
{
|
||||
|
@ -1,12 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Stripe;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
@ -16,6 +15,7 @@ public class UpcomingInvoiceHandler(
|
||||
ILogger<StripeEventProcessor> logger,
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPricingClient pricingClient,
|
||||
IProviderRepository providerRepository,
|
||||
IStripeFacade stripeFacade,
|
||||
IStripeEventService stripeEventService,
|
||||
@ -52,7 +52,9 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
await TryEnableAutomaticTaxAsync(subscription);
|
||||
|
||||
if (!HasAnnualPlan(organization))
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.IsAnnual)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -136,7 +138,7 @@ public class UpcomingInvoiceHandler(
|
||||
{
|
||||
if (subscription.AutomaticTax.Enabled ||
|
||||
!subscription.Customer.HasBillingLocation() ||
|
||||
IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
||||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -150,14 +152,12 @@ public class UpcomingInvoiceHandler(
|
||||
|
||||
return;
|
||||
|
||||
bool IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
|
||||
async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
|
||||
{
|
||||
var familyPriceIds = new List<string>
|
||||
{
|
||||
// TODO: Replace with the PricingClient
|
||||
StaticStore.GetPlan(PlanType.FamiliesAnnually2019).PasswordManager.StripePlanId,
|
||||
StaticStore.GetPlan(PlanType.FamiliesAnnually).PasswordManager.StripePlanId
|
||||
};
|
||||
var familyPriceIds = (await Task.WhenAll(
|
||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
|
||||
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
|
||||
.Select(plan => plan.PasswordManager.StripePlanId);
|
||||
|
||||
return localSubscription.Customer.Address.Country != "US" &&
|
||||
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
|
||||
@ -165,6 +165,4 @@ public class UpcomingInvoiceHandler(
|
||||
!localSubscription.Customer.TaxIds.Any();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasAnnualPlan(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -24,6 +25,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IGroupRepository _groupRepository;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public UpdateOrganizationUserCommand(
|
||||
IEventService eventService,
|
||||
@ -34,7 +36,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
||||
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
|
||||
ICollectionRepository collectionRepository,
|
||||
IGroupRepository groupRepository,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_organizationService = organizationService;
|
||||
@ -45,6 +48,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
||||
_collectionRepository = collectionRepository;
|
||||
_groupRepository = groupRepository;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -128,8 +132,10 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
|
||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organizationUser.OrganizationId, 1);
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-17012
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -45,11 +46,12 @@ public class CloudOrganizationSignUpCommand(
|
||||
IPushRegistrationService pushRegistrationService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ICollectionRepository collectionRepository,
|
||||
IDeviceRepository deviceRepository) : ICloudOrganizationSignUpCommand
|
||||
IDeviceRepository deviceRepository,
|
||||
IPricingClient pricingClient) : ICloudOrganizationSignUpCommand
|
||||
{
|
||||
public async Task<SignUpOrganizationResponse> SignUpOrganizationAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = await pricingClient.GetPlanOrThrow(signup.Plan);
|
||||
|
||||
ValidatePasswordManagerPlan(plan, signup);
|
||||
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -74,6 +75,7 @@ public class OrganizationService : IOrganizationService
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public OrganizationService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -108,7 +110,8 @@ public class OrganizationService : IOrganizationService
|
||||
IFeatureService featureService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
|
||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -143,6 +146,7 @@ public class OrganizationService : IOrganizationService
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
|
||||
@ -210,11 +214,7 @@ public class OrganizationService : IOrganizationService
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.PasswordManager.HasAdditionalStorageOption)
|
||||
{
|
||||
@ -268,7 +268,7 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException($"Cannot set max seat autoscaling below current seat count.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
@ -320,11 +320,7 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException("No subscription found.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
@ -442,7 +438,7 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
|
||||
{
|
||||
var plan = StaticStore.GetPlan(signup.Plan);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
|
||||
|
||||
ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
|
||||
|
||||
@ -530,17 +526,6 @@ public class OrganizationService : IOrganizationService
|
||||
throw new BadRequestException(exception);
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType);
|
||||
if (plan is null)
|
||||
{
|
||||
throw new BadRequestException($"Server must be updated to support {license.Plan}.");
|
||||
}
|
||||
|
||||
if (license.PlanType != PlanType.Custom && plan.Disabled)
|
||||
{
|
||||
throw new BadRequestException($"Plan {plan.Name} is disabled.");
|
||||
}
|
||||
|
||||
var enabledOrgs = await _organizationRepository.GetManyByEnabledAsync();
|
||||
if (enabledOrgs.Any(o => string.Equals(o.LicenseKey, license.LicenseKey)))
|
||||
{
|
||||
@ -882,7 +867,8 @@ public class OrganizationService : IOrganizationService
|
||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(organization.Id, inviteWithSmAccessCount);
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
smSubscriptionUpdate = new SecretsManagerSubscriptionUpdate(organization, plan, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
}
|
||||
|
||||
@ -1008,7 +994,8 @@ public class OrganizationService : IOrganizationService
|
||||
if (initialSmSeatCount.HasValue && currentOrganization.SmSeats.HasValue &&
|
||||
currentOrganization.SmSeats.Value != initialSmSeatCount.Value)
|
||||
{
|
||||
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, false)
|
||||
var plan = await _pricingClient.GetPlanOrThrow(currentOrganization.PlanType);
|
||||
var smSubscriptionUpdateRevert = new SecretsManagerSubscriptionUpdate(currentOrganization, plan, false)
|
||||
{
|
||||
SmSeats = initialSmSeatCount.Value
|
||||
};
|
||||
@ -2237,13 +2224,6 @@ public class OrganizationService : IOrganizationService
|
||||
|
||||
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
||||
{
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||
|
||||
if (plan!.Disabled)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
}
|
||||
|
||||
organization.Id = CoreHelpers.GenerateComb();
|
||||
organization.Enabled = false;
|
||||
organization.Status = OrganizationStatusType.Pending;
|
||||
|
@ -34,6 +34,7 @@ public static class StripeConstants
|
||||
public static class InvoiceStatus
|
||||
{
|
||||
public const string Draft = "draft";
|
||||
public const string Open = "open";
|
||||
}
|
||||
|
||||
public static class MetadataKeys
|
||||
|
@ -10,6 +10,17 @@ namespace Bit.Core.Billing.Extensions;
|
||||
|
||||
public static class BillingExtensions
|
||||
{
|
||||
public static ProductTierType GetProductTier(this PlanType planType)
|
||||
=> planType switch
|
||||
{
|
||||
PlanType.Custom or PlanType.Free => ProductTierType.Free,
|
||||
PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2019 => ProductTierType.Families,
|
||||
PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter,
|
||||
_ when planType.ToString().Contains("Teams") => ProductTierType.Teams,
|
||||
_ when planType.ToString().Contains("Enterprise") => ProductTierType.Enterprise,
|
||||
_ => throw new BillingException($"PlanType {planType} could not be matched to a ProductTierType")
|
||||
};
|
||||
|
||||
public static bool IsBillable(this Provider provider) =>
|
||||
provider is
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Caches.Implementations;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
|
||||
@ -17,7 +18,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
|
||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||
// services.AddSingleton<IPricingClient, PricingClient>();
|
||||
services.AddLicenseServices();
|
||||
services.AddPricingClient();
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,11 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Migration.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
@ -19,6 +19,7 @@ public class OrganizationMigrator(
|
||||
ILogger<OrganizationMigrator> logger,
|
||||
IMigrationTrackerCache migrationTrackerCache,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter) : IOrganizationMigrator
|
||||
{
|
||||
private const string _cancellationComment = "Cancelled as part of provider migration to Consolidated Billing";
|
||||
@ -137,7 +138,7 @@ public class OrganizationMigrator(
|
||||
logger.LogInformation("CB: Bringing organization ({OrganizationID}) under provider management",
|
||||
organization.Id);
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly);
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.Plan.Contains("Teams") ? PlanType.TeamsMonthly : PlanType.EnterpriseMonthly);
|
||||
|
||||
ResetOrganizationPlan(organization, plan);
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
@ -206,7 +207,7 @@ public class OrganizationMigrator(
|
||||
? StripeConstants.CollectionMethod.ChargeAutomatically
|
||||
: StripeConstants.CollectionMethod.SendInvoice;
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
var items = new List<SubscriptionItemOptions>
|
||||
{
|
||||
@ -279,7 +280,7 @@ public class OrganizationMigrator(
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetPlan(migrationRecord.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(migrationRecord.PlanType);
|
||||
|
||||
ResetOrganizationPlan(organization, plan);
|
||||
organization.MaxStorageGb = migrationRecord.MaxStorageGb;
|
||||
|
@ -1,24 +1,11 @@
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record ConfiguredProviderPlan(
|
||||
Guid Id,
|
||||
Guid ProviderId,
|
||||
PlanType PlanType,
|
||||
Plan Plan,
|
||||
int SeatMinimum,
|
||||
int PurchasedSeats,
|
||||
int AssignedSeats)
|
||||
{
|
||||
public static ConfiguredProviderPlan From(ProviderPlan providerPlan) =>
|
||||
providerPlan.IsConfigured()
|
||||
? new ConfiguredProviderPlan(
|
||||
providerPlan.Id,
|
||||
providerPlan.ProviderId,
|
||||
providerPlan.PlanType,
|
||||
providerPlan.SeatMinimum.GetValueOrDefault(0),
|
||||
providerPlan.PurchasedSeats.GetValueOrDefault(0),
|
||||
providerPlan.AllocatedSeats.GetValueOrDefault(0))
|
||||
: null;
|
||||
}
|
||||
int AssignedSeats);
|
||||
|
@ -10,4 +10,17 @@ public record OrganizationMetadata(
|
||||
bool IsSubscriptionCanceled,
|
||||
DateTime? InvoiceDueDate,
|
||||
DateTime? InvoiceCreatedDate,
|
||||
DateTime? SubPeriodEndDate);
|
||||
DateTime? SubPeriodEndDate)
|
||||
{
|
||||
public static OrganizationMetadata Default => new OrganizationMetadata(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
@ -76,8 +76,6 @@ public class OrganizationSale
|
||||
|
||||
private static SubscriptionSetup GetSubscriptionSetup(OrganizationUpgrade upgrade)
|
||||
{
|
||||
var plan = Core.Utilities.StaticStore.GetPlan(upgrade.Plan);
|
||||
|
||||
var passwordManagerOptions = new SubscriptionSetup.PasswordManager
|
||||
{
|
||||
Seats = upgrade.AdditionalSeats,
|
||||
@ -95,7 +93,7 @@ public class OrganizationSale
|
||||
|
||||
return new SubscriptionSetup
|
||||
{
|
||||
Plan = plan,
|
||||
PlanType = upgrade.Plan,
|
||||
PasswordManagerOptions = passwordManagerOptions,
|
||||
SecretsManagerOptions = secretsManagerOptions
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Bit.Core.Billing.Models.Sales;
|
||||
|
||||
public class SubscriptionSetup
|
||||
{
|
||||
public required Plan Plan { get; set; }
|
||||
public required PlanType PlanType { get; set; }
|
||||
public required PasswordManager PasswordManagerOptions { get; set; }
|
||||
public SecretsManager? SecretsManagerOptions { get; set; }
|
||||
public bool SkipTrial = false;
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -7,6 +9,30 @@ namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
public interface IPricingClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
/// </summary>
|
||||
/// <param name="planType">The type of plan to retrieve.</param>
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record or null in the case the plan could not be found or the method was executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan?> GetPlan(PlanType planType);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve a Bitwarden plan by its <paramref name="planType"/>. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
/// </summary>
|
||||
/// <param name="planType">The type of plan to retrieve.</param>
|
||||
/// <returns>A Bitwarden <see cref="Plan"/> record.</returns>
|
||||
/// <exception cref="NotFoundException">Thrown when the <see cref="Plan"/> for the provided <paramref name="planType"/> could not be found or the method was executed from a self-hosted instance.</exception>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<Plan> GetPlanOrThrow(PlanType planType);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all the Bitwarden plans. If the feature flag 'use-pricing-service' is enabled,
|
||||
/// this will trigger a request to the Bitwarden Pricing Service. Otherwise, it will use the existing <see cref="StaticStore"/>.
|
||||
/// </summary>
|
||||
/// <returns>A list of Bitwarden <see cref="Plan"/> records or an empty list in the case the method is executed from a self-hosted instance.</returns>
|
||||
/// <exception cref="BillingException">Thrown when the request to the Pricing Service fails unexpectedly.</exception>
|
||||
Task<List<Plan>> ListPlans();
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.JSON;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class FreeOrScalableDTOJsonConverter : TypeReadingJsonConverter<FreeOrScalableDTO>
|
||||
{
|
||||
public override FreeOrScalableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var type = ReadType(reader);
|
||||
|
||||
return type switch
|
||||
{
|
||||
"free" => JsonSerializer.Deserialize<FreeDTO>(ref reader, options) switch
|
||||
{
|
||||
null => null,
|
||||
var free => new FreeOrScalableDTO(free)
|
||||
},
|
||||
"scalable" => JsonSerializer.Deserialize<ScalableDTO>(ref reader, options) switch
|
||||
{
|
||||
null => null,
|
||||
var scalable => new FreeOrScalableDTO(scalable)
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, FreeOrScalableDTO value, JsonSerializerOptions options)
|
||||
=> value.Switch(
|
||||
free => JsonSerializer.Serialize(writer, free, options),
|
||||
scalable => JsonSerializer.Serialize(writer, scalable, options)
|
||||
);
|
||||
}
|
40
src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs
Normal file
40
src/Core/Billing/Pricing/JSON/PurchasableDTOJsonConverter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.JSON;
|
||||
|
||||
#nullable enable
|
||||
internal class PurchasableDTOJsonConverter : TypeReadingJsonConverter<PurchasableDTO>
|
||||
{
|
||||
public override PurchasableDTO? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var type = ReadType(reader);
|
||||
|
||||
return type switch
|
||||
{
|
||||
"free" => JsonSerializer.Deserialize<FreeDTO>(ref reader, options) switch
|
||||
{
|
||||
null => null,
|
||||
var free => new PurchasableDTO(free)
|
||||
},
|
||||
"packaged" => JsonSerializer.Deserialize<PackagedDTO>(ref reader, options) switch
|
||||
{
|
||||
null => null,
|
||||
var packaged => new PurchasableDTO(packaged)
|
||||
},
|
||||
"scalable" => JsonSerializer.Deserialize<ScalableDTO>(ref reader, options) switch
|
||||
{
|
||||
null => null,
|
||||
var scalable => new PurchasableDTO(scalable)
|
||||
},
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PurchasableDTO value, JsonSerializerOptions options)
|
||||
=> value.Switch(
|
||||
free => JsonSerializer.Serialize(writer, free, options),
|
||||
packaged => JsonSerializer.Serialize(writer, packaged, options),
|
||||
scalable => JsonSerializer.Serialize(writer, scalable, options)
|
||||
);
|
||||
}
|
28
src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs
Normal file
28
src/Core/Billing/Pricing/JSON/TypeReadingJsonConverter.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.JSON;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public abstract class TypeReadingJsonConverter<T> : JsonConverter<T>
|
||||
{
|
||||
protected virtual string TypePropertyName => nameof(ScalableDTO.Type).ToLower();
|
||||
|
||||
protected string? ReadType(Utf8JsonReader reader)
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.PropertyName || reader.GetString()?.ToLower() != TypePropertyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
return reader.GetString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
9
src/Core/Billing/Pricing/Models/FeatureDTO.cs
Normal file
9
src/Core/Billing/Pricing/Models/FeatureDTO.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class FeatureDTO
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public string LookupKey { get; set; } = null!;
|
||||
}
|
27
src/Core/Billing/Pricing/Models/PlanDTO.cs
Normal file
27
src/Core/Billing/Pricing/Models/PlanDTO.cs
Normal file
@ -0,0 +1,27 @@
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class PlanDTO
|
||||
{
|
||||
public string LookupKey { get; set; } = null!;
|
||||
public string Name { get; set; } = null!;
|
||||
public string Tier { get; set; } = null!;
|
||||
public string? Cadence { get; set; }
|
||||
public int? LegacyYear { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public FeatureDTO[] Features { get; set; } = null!;
|
||||
public PurchasableDTO Seats { get; set; } = null!;
|
||||
public ScalableDTO? ManagedSeats { get; set; }
|
||||
public ScalableDTO? Storage { get; set; }
|
||||
public SecretsManagerPurchasablesDTO? SecretsManager { get; set; }
|
||||
public int? TrialPeriodDays { get; set; }
|
||||
public string[] CanUpgradeTo { get; set; } = null!;
|
||||
public Dictionary<string, string> AdditionalData { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class SecretsManagerPurchasablesDTO
|
||||
{
|
||||
public FreeOrScalableDTO Seats { get; set; } = null!;
|
||||
public FreeOrScalableDTO ServiceAccounts { get; set; } = null!;
|
||||
}
|
73
src/Core/Billing/Pricing/Models/PurchasableDTO.cs
Normal file
73
src/Core/Billing/Pricing/Models/PurchasableDTO.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Billing.Pricing.JSON;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing.Models;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[JsonConverter(typeof(PurchasableDTOJsonConverter))]
|
||||
public class PurchasableDTO(OneOf<FreeDTO, PackagedDTO, ScalableDTO> input) : OneOfBase<FreeDTO, PackagedDTO, ScalableDTO>(input)
|
||||
{
|
||||
public static implicit operator PurchasableDTO(FreeDTO free) => new(free);
|
||||
public static implicit operator PurchasableDTO(PackagedDTO packaged) => new(packaged);
|
||||
public static implicit operator PurchasableDTO(ScalableDTO scalable) => new(scalable);
|
||||
|
||||
public T? FromFree<T>(Func<FreeDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
|
||||
IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;
|
||||
|
||||
public T? FromPackaged<T>(Func<PackagedDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
|
||||
IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;
|
||||
|
||||
public T? FromScalable<T>(Func<ScalableDTO, T> select, Func<PurchasableDTO, T>? fallback = null) =>
|
||||
IsT2 ? select(AsT2) : fallback != null ? fallback(this) : default;
|
||||
|
||||
public bool IsFree => IsT0;
|
||||
public bool IsPackaged => IsT1;
|
||||
public bool IsScalable => IsT2;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(FreeOrScalableDTOJsonConverter))]
|
||||
public class FreeOrScalableDTO(OneOf<FreeDTO, ScalableDTO> input) : OneOfBase<FreeDTO, ScalableDTO>(input)
|
||||
{
|
||||
public static implicit operator FreeOrScalableDTO(FreeDTO freeDTO) => new(freeDTO);
|
||||
public static implicit operator FreeOrScalableDTO(ScalableDTO scalableDTO) => new(scalableDTO);
|
||||
|
||||
public T? FromFree<T>(Func<FreeDTO, T> select, Func<FreeOrScalableDTO, T>? fallback = null) =>
|
||||
IsT0 ? select(AsT0) : fallback != null ? fallback(this) : default;
|
||||
|
||||
public T? FromScalable<T>(Func<ScalableDTO, T> select, Func<FreeOrScalableDTO, T>? fallback = null) =>
|
||||
IsT1 ? select(AsT1) : fallback != null ? fallback(this) : default;
|
||||
|
||||
public bool IsFree => IsT0;
|
||||
public bool IsScalable => IsT1;
|
||||
}
|
||||
|
||||
public class FreeDTO
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public string Type => "free";
|
||||
}
|
||||
|
||||
public class PackagedDTO
|
||||
{
|
||||
public int Quantity { get; set; }
|
||||
public string StripePriceId { get; set; } = null!;
|
||||
public decimal Price { get; set; }
|
||||
public AdditionalSeats? Additional { get; set; }
|
||||
public string Type => "packaged";
|
||||
|
||||
public class AdditionalSeats
|
||||
{
|
||||
public string StripePriceId { get; set; } = null!;
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public class ScalableDTO
|
||||
{
|
||||
public int Provided { get; set; }
|
||||
public string StripePriceId { get; set; } = null!;
|
||||
public decimal Price { get; set; }
|
||||
public string Type => "scalable";
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using Proto.Billing.Pricing;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -8,15 +8,15 @@ namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
public record PlanAdapter : Plan
|
||||
{
|
||||
public PlanAdapter(PlanResponse planResponse)
|
||||
public PlanAdapter(PlanDTO plan)
|
||||
{
|
||||
Type = ToPlanType(planResponse.LookupKey);
|
||||
Type = ToPlanType(plan.LookupKey);
|
||||
ProductTier = ToProductTierType(Type);
|
||||
Name = planResponse.Name;
|
||||
IsAnnual = !string.IsNullOrEmpty(planResponse.Cadence) && planResponse.Cadence == "annually";
|
||||
NameLocalizationKey = planResponse.AdditionalData?["nameLocalizationKey"];
|
||||
DescriptionLocalizationKey = planResponse.AdditionalData?["descriptionLocalizationKey"];
|
||||
TrialPeriodDays = planResponse.TrialPeriodDays;
|
||||
Name = plan.Name;
|
||||
IsAnnual = plan.Cadence is "annually";
|
||||
NameLocalizationKey = plan.AdditionalData["nameLocalizationKey"];
|
||||
DescriptionLocalizationKey = plan.AdditionalData["descriptionLocalizationKey"];
|
||||
TrialPeriodDays = plan.TrialPeriodDays;
|
||||
HasSelfHost = HasFeature("selfHost");
|
||||
HasPolicies = HasFeature("policies");
|
||||
HasGroups = HasFeature("groups");
|
||||
@ -30,20 +30,20 @@ public record PlanAdapter : Plan
|
||||
HasScim = HasFeature("scim");
|
||||
HasResetPassword = HasFeature("resetPassword");
|
||||
UsersGetPremium = HasFeature("usersGetPremium");
|
||||
UpgradeSortOrder = planResponse.AdditionalData != null
|
||||
? int.Parse(planResponse.AdditionalData["upgradeSortOrder"])
|
||||
UpgradeSortOrder = plan.AdditionalData.TryGetValue("upgradeSortOrder", out var upgradeSortOrder)
|
||||
? int.Parse(upgradeSortOrder)
|
||||
: 0;
|
||||
DisplaySortOrder = planResponse.AdditionalData != null
|
||||
? int.Parse(planResponse.AdditionalData["displaySortOrder"])
|
||||
DisplaySortOrder = plan.AdditionalData.TryGetValue("displaySortOrder", out var displaySortOrder)
|
||||
? int.Parse(displaySortOrder)
|
||||
: 0;
|
||||
HasCustomPermissions = HasFeature("customPermissions");
|
||||
Disabled = !planResponse.Available;
|
||||
PasswordManager = ToPasswordManagerPlanFeatures(planResponse);
|
||||
SecretsManager = planResponse.SecretsManager != null ? ToSecretsManagerPlanFeatures(planResponse) : null;
|
||||
Disabled = !plan.Available;
|
||||
LegacyYear = plan.LegacyYear;
|
||||
PasswordManager = ToPasswordManagerPlanFeatures(plan);
|
||||
SecretsManager = plan.SecretsManager != null ? ToSecretsManagerPlanFeatures(plan) : null;
|
||||
|
||||
return;
|
||||
|
||||
bool HasFeature(string lookupKey) => planResponse.Features.Any(feature => feature.LookupKey == lookupKey);
|
||||
bool HasFeature(string lookupKey) => plan.Features.Any(feature => feature.LookupKey == lookupKey);
|
||||
}
|
||||
|
||||
#region Mappings
|
||||
@ -86,29 +86,25 @@ public record PlanAdapter : Plan
|
||||
_ => throw new BillingException() // TODO: Flesh out
|
||||
};
|
||||
|
||||
private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanResponse planResponse)
|
||||
private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanDTO plan)
|
||||
{
|
||||
var stripePlanId = GetStripePlanId(planResponse.Seats);
|
||||
var stripeSeatPlanId = GetStripeSeatPlanId(planResponse.Seats);
|
||||
var stripeProviderPortalSeatPlanId = planResponse.ManagedSeats?.StripePriceId;
|
||||
var basePrice = GetBasePrice(planResponse.Seats);
|
||||
var seatPrice = GetSeatPrice(planResponse.Seats);
|
||||
var providerPortalSeatPrice =
|
||||
planResponse.ManagedSeats != null ? decimal.Parse(planResponse.ManagedSeats.Price) : 0;
|
||||
var scales = planResponse.Seats.KindCase switch
|
||||
{
|
||||
PurchasableDTO.KindOneofCase.Scalable => true,
|
||||
PurchasableDTO.KindOneofCase.Packaged => planResponse.Seats.Packaged.Additional != null,
|
||||
_ => false
|
||||
};
|
||||
var baseSeats = GetBaseSeats(planResponse.Seats);
|
||||
var maxSeats = GetMaxSeats(planResponse.Seats);
|
||||
var baseStorageGb = (short?)planResponse.Storage?.Provided;
|
||||
var hasAdditionalStorageOption = planResponse.Storage != null;
|
||||
var stripeStoragePlanId = planResponse.Storage?.StripePriceId;
|
||||
short? maxCollections =
|
||||
planResponse.AdditionalData != null &&
|
||||
planResponse.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null;
|
||||
var stripePlanId = GetStripePlanId(plan.Seats);
|
||||
var stripeSeatPlanId = GetStripeSeatPlanId(plan.Seats);
|
||||
var stripeProviderPortalSeatPlanId = plan.ManagedSeats?.StripePriceId;
|
||||
var basePrice = GetBasePrice(plan.Seats);
|
||||
var seatPrice = GetSeatPrice(plan.Seats);
|
||||
var providerPortalSeatPrice = plan.ManagedSeats?.Price ?? 0;
|
||||
var scales = plan.Seats.Match(
|
||||
_ => false,
|
||||
packaged => packaged.Additional != null,
|
||||
_ => true);
|
||||
var baseSeats = GetBaseSeats(plan.Seats);
|
||||
var maxSeats = GetMaxSeats(plan.Seats);
|
||||
var baseStorageGb = (short?)plan.Storage?.Provided;
|
||||
var hasAdditionalStorageOption = plan.Storage != null;
|
||||
var additionalStoragePricePerGb = plan.Storage?.Price ?? 0;
|
||||
var stripeStoragePlanId = plan.Storage?.StripePriceId;
|
||||
short? maxCollections = plan.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null;
|
||||
|
||||
return new PasswordManagerPlanFeatures
|
||||
{
|
||||
@ -124,30 +120,29 @@ public record PlanAdapter : Plan
|
||||
MaxSeats = maxSeats,
|
||||
BaseStorageGb = baseStorageGb,
|
||||
HasAdditionalStorageOption = hasAdditionalStorageOption,
|
||||
AdditionalStoragePricePerGb = additionalStoragePricePerGb,
|
||||
StripeStoragePlanId = stripeStoragePlanId,
|
||||
MaxCollections = maxCollections
|
||||
};
|
||||
}
|
||||
|
||||
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanResponse planResponse)
|
||||
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanDTO plan)
|
||||
{
|
||||
var seats = planResponse.SecretsManager.Seats;
|
||||
var serviceAccounts = planResponse.SecretsManager.ServiceAccounts;
|
||||
var seats = plan.SecretsManager!.Seats;
|
||||
var serviceAccounts = plan.SecretsManager.ServiceAccounts;
|
||||
|
||||
var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);
|
||||
var allowServiceAccountsAutoscale = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||
var allowServiceAccountsAutoscale = serviceAccounts.IsScalable;
|
||||
var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);
|
||||
var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);
|
||||
var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);
|
||||
var hasAdditionalServiceAccountOption = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||
var hasAdditionalServiceAccountOption = serviceAccounts.IsScalable;
|
||||
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
|
||||
var hasAdditionalSeatsOption = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||
var hasAdditionalSeatsOption = seats.IsScalable;
|
||||
var seatPrice = GetSeatPrice(seats);
|
||||
var maxSeats = GetMaxSeats(seats);
|
||||
var allowSeatAutoscale = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
|
||||
var maxProjects =
|
||||
planResponse.AdditionalData != null &&
|
||||
planResponse.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
|
||||
var allowSeatAutoscale = seats.IsScalable;
|
||||
var maxProjects = plan.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
|
||||
|
||||
return new SecretsManagerPlanFeatures
|
||||
{
|
||||
@ -167,66 +162,54 @@ public record PlanAdapter : Plan
|
||||
}
|
||||
|
||||
private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||
? null
|
||||
: decimal.Parse(freeOrScalable.Scalable.Price);
|
||||
=> freeOrScalable.FromScalable(x => x.Price);
|
||||
|
||||
private static decimal GetBasePrice(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : decimal.Parse(purchasable.Packaged.Price);
|
||||
=> purchasable.FromPackaged(x => x.Price);
|
||||
|
||||
private static int GetBaseSeats(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : purchasable.Packaged.Quantity;
|
||||
=> purchasable.FromPackaged(x => x.Quantity);
|
||||
|
||||
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase switch
|
||||
{
|
||||
FreeOrScalableDTO.KindOneofCase.Free => (short)freeOrScalable.Free.Quantity,
|
||||
FreeOrScalableDTO.KindOneofCase.Scalable => (short)freeOrScalable.Scalable.Provided,
|
||||
_ => 0
|
||||
};
|
||||
=> freeOrScalable.Match(
|
||||
free => (short)free.Quantity,
|
||||
scalable => (short)scalable.Provided);
|
||||
|
||||
private static short? GetMaxSeats(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Free ? null : (short)purchasable.Free.Quantity;
|
||||
=> purchasable.Match<short?>(
|
||||
free => (short)free.Quantity,
|
||||
packaged => (short)packaged.Quantity,
|
||||
_ => null);
|
||||
|
||||
private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity;
|
||||
=> freeOrScalable.FromFree(x => (short)x.Quantity);
|
||||
|
||||
private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity;
|
||||
=> freeOrScalable.FromFree(x => (short)x.Quantity);
|
||||
|
||||
private static decimal GetSeatPrice(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase switch
|
||||
{
|
||||
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional != null ? decimal.Parse(purchasable.Packaged.Additional.Price) : 0,
|
||||
PurchasableDTO.KindOneofCase.Scalable => decimal.Parse(purchasable.Scalable.Price),
|
||||
_ => 0
|
||||
};
|
||||
=> purchasable.Match(
|
||||
_ => 0,
|
||||
packaged => packaged.Additional?.Price ?? 0,
|
||||
scalable => scalable.Price);
|
||||
|
||||
private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||
? 0
|
||||
: decimal.Parse(freeOrScalable.Scalable.Price);
|
||||
=> freeOrScalable.FromScalable(x => x.Price);
|
||||
|
||||
private static string? GetStripePlanId(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? null : purchasable.Packaged.StripePriceId;
|
||||
=> purchasable.FromPackaged(x => x.StripePriceId);
|
||||
|
||||
private static string? GetStripeSeatPlanId(PurchasableDTO purchasable)
|
||||
=> purchasable.KindCase switch
|
||||
{
|
||||
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional?.StripePriceId,
|
||||
PurchasableDTO.KindOneofCase.Scalable => purchasable.Scalable.StripePriceId,
|
||||
_ => null
|
||||
};
|
||||
=> purchasable.Match(
|
||||
_ => null,
|
||||
packaged => packaged.Additional?.StripePriceId,
|
||||
scalable => scalable.StripePriceId);
|
||||
|
||||
private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||
? null
|
||||
: freeOrScalable.Scalable.StripePriceId;
|
||||
=> freeOrScalable.FromScalable(x => x.StripePriceId);
|
||||
|
||||
private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable)
|
||||
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
|
||||
? null
|
||||
: freeOrScalable.Scalable.StripePriceId;
|
||||
=> freeOrScalable.FromScalable(x => x.StripePriceId);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing.Models;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Grpc.Core;
|
||||
using Grpc.Net.Client;
|
||||
using Proto.Billing.Pricing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
#nullable enable
|
||||
|
||||
@ -14,10 +15,17 @@ namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
public class PricingClient(
|
||||
IFeatureService featureService,
|
||||
GlobalSettings globalSettings) : IPricingClient
|
||||
GlobalSettings globalSettings,
|
||||
HttpClient httpClient,
|
||||
ILogger<PricingClient> logger) : IPricingClient
|
||||
{
|
||||
public async Task<Plan?> GetPlan(PlanType planType)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
|
||||
if (!usePricingService)
|
||||
@ -25,30 +33,55 @@ public class PricingClient(
|
||||
return StaticStore.GetPlan(planType);
|
||||
}
|
||||
|
||||
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
|
||||
var client = new PasswordManager.PasswordManagerClient(channel);
|
||||
var lookupKey = GetLookupKey(planType);
|
||||
|
||||
var lookupKey = ToLookupKey(planType);
|
||||
if (string.IsNullOrEmpty(lookupKey))
|
||||
if (lookupKey == null)
|
||||
{
|
||||
logger.LogError("Could not find Pricing Service lookup key for PlanType {PlanType}", planType);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response =
|
||||
await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey });
|
||||
var response = await httpClient.GetAsync($"plans/lookup/{lookupKey}");
|
||||
|
||||
return new PlanAdapter(response);
|
||||
}
|
||||
catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plan = await response.Content.ReadFromJsonAsync<PlanDTO>();
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return new PlanAdapter(plan);
|
||||
}
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
{
|
||||
logger.LogError("Pricing Service plan for PlanType {PlanType} was not found", planType);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
message: $"Request to the Pricing Service failed with status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
public async Task<Plan> GetPlanOrThrow(PlanType planType)
|
||||
{
|
||||
var plan = await GetPlan(planType);
|
||||
|
||||
if (plan == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
public async Task<List<Plan>> ListPlans()
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
|
||||
|
||||
if (!usePricingService)
|
||||
@ -56,14 +89,23 @@ public class PricingClient(
|
||||
return StaticStore.Plans.ToList();
|
||||
}
|
||||
|
||||
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
|
||||
var client = new PasswordManager.PasswordManagerClient(channel);
|
||||
var response = await httpClient.GetAsync("plans");
|
||||
|
||||
var response = await client.ListPlansAsync(new Empty());
|
||||
return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var plans = await response.Content.ReadFromJsonAsync<List<PlanDTO>>();
|
||||
if (plans == null)
|
||||
{
|
||||
throw new BillingException(message: "Deserialization of Pricing Service response resulted in null");
|
||||
}
|
||||
return plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
message: $"Request to the Pricing Service failed with status {response.StatusCode}");
|
||||
}
|
||||
|
||||
private static string? ToLookupKey(PlanType planType)
|
||||
private static string? GetLookupKey(PlanType planType)
|
||||
=> planType switch
|
||||
{
|
||||
PlanType.EnterpriseAnnually => "enterprise-annually",
|
||||
|
@ -1,92 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Proto.Billing.Pricing";
|
||||
|
||||
package plans;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
service PasswordManager {
|
||||
rpc GetPlanByLookupKey (GetPlanByLookupKeyRequest) returns (PlanResponse);
|
||||
rpc ListPlans (google.protobuf.Empty) returns (ListPlansResponse);
|
||||
}
|
||||
|
||||
// Requests
|
||||
message GetPlanByLookupKeyRequest {
|
||||
string lookupKey = 1;
|
||||
}
|
||||
|
||||
// Responses
|
||||
message PlanResponse {
|
||||
string name = 1;
|
||||
string lookupKey = 2;
|
||||
string tier = 4;
|
||||
optional string cadence = 6;
|
||||
optional google.protobuf.Int32Value legacyYear = 8;
|
||||
bool available = 9;
|
||||
repeated FeatureDTO features = 10;
|
||||
PurchasableDTO seats = 11;
|
||||
optional ScalableDTO managedSeats = 12;
|
||||
optional ScalableDTO storage = 13;
|
||||
optional SecretsManagerPurchasablesDTO secretsManager = 14;
|
||||
optional google.protobuf.Int32Value trialPeriodDays = 15;
|
||||
repeated string canUpgradeTo = 16;
|
||||
map<string, string> additionalData = 17;
|
||||
}
|
||||
|
||||
message ListPlansResponse {
|
||||
repeated PlanResponse plans = 1;
|
||||
}
|
||||
|
||||
// DTOs
|
||||
message FeatureDTO {
|
||||
string name = 1;
|
||||
string lookupKey = 2;
|
||||
}
|
||||
|
||||
message FreeDTO {
|
||||
int32 quantity = 2;
|
||||
string type = 4;
|
||||
}
|
||||
|
||||
message PackagedDTO {
|
||||
message AdditionalSeats {
|
||||
string stripePriceId = 1;
|
||||
string price = 2;
|
||||
}
|
||||
|
||||
int32 quantity = 2;
|
||||
string stripePriceId = 3;
|
||||
string price = 4;
|
||||
optional AdditionalSeats additional = 5;
|
||||
string type = 6;
|
||||
}
|
||||
|
||||
message ScalableDTO {
|
||||
int32 provided = 2;
|
||||
string stripePriceId = 6;
|
||||
string price = 7;
|
||||
string type = 9;
|
||||
}
|
||||
|
||||
message PurchasableDTO {
|
||||
oneof kind {
|
||||
FreeDTO free = 1;
|
||||
PackagedDTO packaged = 2;
|
||||
ScalableDTO scalable = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message FreeOrScalableDTO {
|
||||
oneof kind {
|
||||
FreeDTO free = 1;
|
||||
ScalableDTO scalable = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message SecretsManagerPurchasablesDTO {
|
||||
FreeOrScalableDTO seats = 1;
|
||||
FreeOrScalableDTO serviceAccounts = 2;
|
||||
}
|
21
src/Core/Billing/Pricing/ServiceCollectionExtensions.cs
Normal file
21
src/Core/Billing/Pricing/ServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Billing.Pricing;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddPricingClient(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient<IPricingClient, PricingClient>((serviceProvider, httpClient) =>
|
||||
{
|
||||
var globalSettings = serviceProvider.GetRequiredService<GlobalSettings>();
|
||||
if (string.IsNullOrEmpty(globalSettings.PricingUri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
httpClient.BaseAddress = new Uri(globalSettings.PricingUri);
|
||||
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
});
|
||||
}
|
||||
}
|
@ -3,12 +3,12 @@ using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Braintree;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
@ -26,6 +26,7 @@ public class OrganizationBillingService(
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPricingClient pricingClient,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
@ -63,13 +64,22 @@ public class OrganizationBillingService(
|
||||
return null;
|
||||
}
|
||||
|
||||
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return OrganizationMetadata.Default;
|
||||
}
|
||||
|
||||
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
|
||||
|
||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false,
|
||||
false, false, false, false, null, null, null);
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||
IsManaged = isManaged
|
||||
};
|
||||
}
|
||||
|
||||
var customer = await subscriberService.GetCustomer(organization,
|
||||
@ -77,18 +87,21 @@ public class OrganizationBillingService(
|
||||
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
|
||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
||||
var isSubscriptionCanceled = IsSubscriptionCanceled(subscription);
|
||||
var hasSubscription = true;
|
||||
var openInvoice = await HasOpenInvoiceAsync(subscription);
|
||||
var hasOpenInvoice = openInvoice.HasOpenInvoice;
|
||||
var invoiceDueDate = openInvoice.DueDate;
|
||||
var invoiceCreatedDate = openInvoice.CreatedDate;
|
||||
var subPeriodEndDate = subscription?.CurrentPeriodEnd;
|
||||
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
||||
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
|
||||
var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
|
||||
|
||||
return new OrganizationMetadata(
|
||||
isEligibleForSelfHost,
|
||||
isManaged,
|
||||
isOnSecretsManagerStandalone,
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid,
|
||||
true,
|
||||
invoice?.Status == StripeConstants.InvoiceStatus.Open,
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Canceled,
|
||||
invoice?.DueDate,
|
||||
invoice?.Created,
|
||||
subscription.CurrentPeriodEnd);
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
@ -299,7 +312,7 @@ public class OrganizationBillingService(
|
||||
Customer customer,
|
||||
SubscriptionSetup subscriptionSetup)
|
||||
{
|
||||
var plan = subscriptionSetup.Plan;
|
||||
var plan = await pricingClient.GetPlanOrThrow(subscriptionSetup.PlanType);
|
||||
|
||||
var passwordManagerOptions = subscriptionSetup.PasswordManagerOptions;
|
||||
|
||||
@ -385,15 +398,17 @@ public class OrganizationBillingService(
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
}
|
||||
|
||||
private static bool IsEligibleForSelfHost(
|
||||
private async Task<bool> IsEligibleForSelfHostAsync(
|
||||
Organization organization)
|
||||
{
|
||||
var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||
var plans = await pricingClient.ListPlans();
|
||||
|
||||
var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||
|
||||
return eligibleSelfHostPlans.Contains(organization.PlanType);
|
||||
}
|
||||
|
||||
private static bool IsOnSecretsManagerStandalone(
|
||||
private async Task<bool> IsOnSecretsManagerStandalone(
|
||||
Organization organization,
|
||||
Customer? customer,
|
||||
Subscription? subscription)
|
||||
@ -403,7 +418,7 @@ public class OrganizationBillingService(
|
||||
return false;
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.SupportsSecretsManager)
|
||||
{
|
||||
@ -424,38 +439,5 @@ public class OrganizationBillingService(
|
||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||
}
|
||||
|
||||
private static bool IsSubscriptionUnpaid(Subscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return subscription.Status == "unpaid";
|
||||
}
|
||||
|
||||
private async Task<(bool HasOpenInvoice, DateTime? CreatedDate, DateTime? DueDate)> HasOpenInvoiceAsync(Subscription subscription)
|
||||
{
|
||||
if (subscription?.LatestInvoiceId == null)
|
||||
{
|
||||
return (false, null, null);
|
||||
}
|
||||
|
||||
var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions());
|
||||
|
||||
return invoice?.Status == "open"
|
||||
? (true, invoice.Created, invoice.DueDate)
|
||||
: (false, null, null);
|
||||
}
|
||||
|
||||
private static bool IsSubscriptionCanceled(Subscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return subscription.Status == "canceled";
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
@ -27,12 +27,6 @@
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.68.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
||||
@ -78,11 +72,7 @@
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources\" />
|
||||
<Folder Include="Properties\" />
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Stripe;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
@ -9,7 +10,7 @@ namespace Bit.Core.Models.Business;
|
||||
/// </summary>
|
||||
public class SubscriptionData
|
||||
{
|
||||
public StaticStore.Plan Plan { get; init; }
|
||||
public Plan Plan { get; init; }
|
||||
public int PurchasedPasswordManagerSeats { get; init; }
|
||||
public bool SubscribedToSecretsManager { get; set; }
|
||||
public int? PurchasedSecretsManagerSeats { get; init; }
|
||||
@ -38,22 +39,24 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
||||
/// in the case of an error.
|
||||
/// </summary>
|
||||
/// <param name="organization">The <see cref="Organization"/> to upgrade.</param>
|
||||
/// <param name="plan">The organization's plan.</param>
|
||||
/// <param name="updatedSubscription">The updates you want to apply to the organization's subscription.</param>
|
||||
public CompleteSubscriptionUpdate(
|
||||
Organization organization,
|
||||
Plan plan,
|
||||
SubscriptionData updatedSubscription)
|
||||
{
|
||||
_currentSubscription = GetSubscriptionDataFor(organization);
|
||||
_currentSubscription = GetSubscriptionDataFor(organization, plan);
|
||||
_updatedSubscription = updatedSubscription;
|
||||
}
|
||||
|
||||
protected override List<string> PlanIds => new()
|
||||
{
|
||||
protected override List<string> PlanIds =>
|
||||
[
|
||||
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
|
||||
@ -94,7 +97,7 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
||||
*/
|
||||
/// <summary>
|
||||
/// Checks whether the updates provided in the <see cref="CompleteSubscriptionUpdate"/>'s constructor
|
||||
/// are actually different than the organization's current <see cref="Subscription"/>.
|
||||
/// are actually different from the organization's current <see cref="Subscription"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||
public override bool UpdateNeeded(Subscription subscription)
|
||||
@ -278,11 +281,8 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
||||
};
|
||||
}
|
||||
|
||||
private static SubscriptionData GetSubscriptionDataFor(Organization organization)
|
||||
{
|
||||
var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
|
||||
|
||||
return new SubscriptionData
|
||||
private static SubscriptionData GetSubscriptionDataFor(Organization organization, Plan plan)
|
||||
=> new()
|
||||
{
|
||||
Plan = plan,
|
||||
PurchasedPasswordManagerSeats = organization.Seats.HasValue
|
||||
@ -299,5 +299,4 @@ public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
||||
? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) :
|
||||
0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Stripe;
|
||||
using Plan = Bit.Core.Models.StaticStore.Plan;
|
||||
|
||||
namespace Bit.Core.Models.Business;
|
||||
|
||||
@ -14,18 +15,16 @@ public class ProviderSubscriptionUpdate : SubscriptionUpdate
|
||||
protected override List<string> PlanIds => [_planId];
|
||||
|
||||
public ProviderSubscriptionUpdate(
|
||||
PlanType planType,
|
||||
Plan plan,
|
||||
int previouslyPurchasedSeats,
|
||||
int newlyPurchasedSeats)
|
||||
{
|
||||
if (!planType.SupportsConsolidatedBilling())
|
||||
if (!plan.Type.SupportsConsolidatedBilling())
|
||||
{
|
||||
throw new BillingException(
|
||||
message: $"Cannot create a {nameof(ProviderSubscriptionUpdate)} for {nameof(PlanType)} that doesn't support consolidated billing");
|
||||
}
|
||||
|
||||
var plan = Utilities.StaticStore.GetPlan(planType);
|
||||
|
||||
_planId = plan.PasswordManager.StripeProviderPortalSeatPlanId;
|
||||
_previouslyPurchasedSeats = previouslyPurchasedSeats;
|
||||
_newlyPurchasedSeats = newlyPurchasedSeats;
|
||||
|
@ -7,6 +7,7 @@ namespace Bit.Core.Models.Business;
|
||||
public class SecretsManagerSubscriptionUpdate
|
||||
{
|
||||
public Organization Organization { get; }
|
||||
public Plan Plan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total seats the organization will have after the update, including any base seats included in the plan
|
||||
@ -49,21 +50,16 @@ public class SecretsManagerSubscriptionUpdate
|
||||
public bool MaxAutoscaleSmSeatsChanged => MaxAutoscaleSmSeats != Organization.MaxAutoscaleSmSeats;
|
||||
public bool MaxAutoscaleSmServiceAccountsChanged =>
|
||||
MaxAutoscaleSmServiceAccounts != Organization.MaxAutoscaleSmServiceAccounts;
|
||||
public Plan Plan => Utilities.StaticStore.GetPlan(Organization.PlanType);
|
||||
public bool SmSeatAutoscaleLimitReached => SmSeats.HasValue && MaxAutoscaleSmSeats.HasValue && SmSeats == MaxAutoscaleSmSeats;
|
||||
|
||||
public bool SmServiceAccountAutoscaleLimitReached => SmServiceAccounts.HasValue &&
|
||||
MaxAutoscaleSmServiceAccounts.HasValue &&
|
||||
SmServiceAccounts == MaxAutoscaleSmServiceAccounts;
|
||||
|
||||
public SecretsManagerSubscriptionUpdate(Organization organization, bool autoscaling)
|
||||
public SecretsManagerSubscriptionUpdate(Organization organization, Plan plan, bool autoscaling)
|
||||
{
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException("Organization is not found.");
|
||||
}
|
||||
|
||||
Organization = organization;
|
||||
Organization = organization ?? throw new NotFoundException("Organization is not found.");
|
||||
Plan = plan;
|
||||
|
||||
if (!Plan.SupportsSecretsManager)
|
||||
{
|
||||
|
@ -82,7 +82,6 @@ public class SubscriptionInfo
|
||||
}
|
||||
|
||||
public bool AddonSubscriptionItem { get; set; }
|
||||
|
||||
public string ProductId { get; set; }
|
||||
public string Name { get; set; }
|
||||
public decimal Amount { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -54,8 +55,9 @@ public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand
|
||||
foreach (var selfHostedSponsorship in sponsorshipsData)
|
||||
{
|
||||
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductTierType;
|
||||
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
|
||||
if (requiredSponsoringProductType == null
|
||||
|| StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value)
|
||||
|| sponsoringOrgProductTier != requiredSponsoringProductType.Value)
|
||||
{
|
||||
continue; // prevent unsupported sponsorships
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
@ -50,9 +51,10 @@ public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand
|
||||
|
||||
// Check org to sponsor's product type
|
||||
var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductTierType;
|
||||
var sponsoredOrganizationProductTier = sponsoredOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (requiredSponsoredProductType == null ||
|
||||
sponsoredOrganization == null ||
|
||||
StaticStore.GetPlan(sponsoredOrganization.PlanType).ProductTier != requiredSponsoredProductType.Value)
|
||||
sponsoredOrganizationProductTier != requiredSponsoredProductType.Value)
|
||||
{
|
||||
throw new BadRequestException("Can only redeem sponsorship offer on families organizations.");
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@ -103,8 +104,6 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
|
||||
return false;
|
||||
}
|
||||
|
||||
var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType);
|
||||
|
||||
if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization))
|
||||
{
|
||||
_logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is disabled for more than 3 months.", sponsoringOrganization.Id);
|
||||
@ -113,7 +112,9 @@ public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSpo
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgPlan.ProductTier)
|
||||
var sponsoringOrgProductTier = sponsoringOrganization.PlanType.GetProductTier();
|
||||
|
||||
if (sponsoredPlan.SponsoringProductTierType != sponsoringOrgProductTier)
|
||||
{
|
||||
_logger.LogWarning("Sponsoring Organization {SponsoringOrganizationId} is not on the required product type.", sponsoringOrganization.Id);
|
||||
await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -31,9 +32,10 @@ public class CreateSponsorshipCommand : ICreateSponsorshipCommand
|
||||
}
|
||||
|
||||
var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductTierType;
|
||||
var sponsoringOrgProductTier = sponsoringOrg.PlanType.GetProductTier();
|
||||
|
||||
if (requiredSponsoringProductType == null ||
|
||||
sponsoringOrg == null ||
|
||||
StaticStore.GetPlan(sponsoringOrg.PlanType).ProductTier != requiredSponsoringProductType.Value)
|
||||
sponsoringOrgProductTier != requiredSponsoringProductType.Value)
|
||||
{
|
||||
throw new BadRequestException("Specified Organization cannot sponsor other organizations.");
|
||||
}
|
||||
|
@ -2,11 +2,11 @@
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
|
||||
@ -15,22 +15,25 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public AddSecretsManagerSubscriptionCommand(
|
||||
IPaymentService paymentService,
|
||||
IOrganizationService organizationService,
|
||||
IProviderRepository providerRepository)
|
||||
IProviderRepository providerRepository,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_paymentService = paymentService;
|
||||
_organizationService = organizationService;
|
||||
_providerRepository = providerRepository;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
public async Task SignUpAsync(Organization organization, int additionalSmSeats,
|
||||
int additionalServiceAccounts)
|
||||
{
|
||||
await ValidateOrganization(organization);
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
var signup = SetOrganizationUpgrade(organization, additionalSmSeats, additionalServiceAccounts);
|
||||
_organizationService.ValidateSecretsManagerPlan(plan, signup);
|
||||
|
||||
@ -73,7 +76,13 @@ public class AddSecretsManagerSubscriptionCommand : IAddSecretsManagerSubscripti
|
||||
throw new BadRequestException("Organization already uses Secrets Manager.");
|
||||
}
|
||||
|
||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType && p.SupportsSecretsManager);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.SupportsSecretsManager)
|
||||
{
|
||||
throw new BadRequestException("Organization's plan does not support Secrets Manager.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId) && plan.ProductTier != ProductTierType.Free)
|
||||
{
|
||||
throw new BadRequestException("No payment method found.");
|
||||
|
@ -6,6 +6,7 @@ using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -18,7 +19,6 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Business;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
|
||||
@ -38,6 +38,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public UpgradeOrganizationPlanCommand(
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
@ -53,7 +54,8 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService)
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_collectionRepository = collectionRepository;
|
||||
@ -69,6 +71,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
_organizationService = organizationService;
|
||||
_featureService = featureService;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<Tuple<bool, string>> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade)
|
||||
@ -84,14 +87,11 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
||||
throw new BadRequestException("Your account has no payment method available.");
|
||||
}
|
||||
|
||||
var existingPlan = StaticStore.GetPlan(organization.PlanType);
|
||||
if (existingPlan == null)
|
||||
{
|
||||
throw new BadRequestException("Existing plan not found.");
|
||||
}
|
||||
var existingPlan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled);
|
||||
if (newPlan == null)
|
||||
var newPlan = await _pricingClient.GetPlanOrThrow(upgrade.Plan);
|
||||
|
||||
if (newPlan.Disabled)
|
||||
{
|
||||
throw new BadRequestException("Plan not found.");
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.Api.Responses;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -37,6 +38,7 @@ public class StripePaymentService : IPaymentService
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITaxService _taxService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public StripePaymentService(
|
||||
ITransactionRepository transactionRepository,
|
||||
@ -46,7 +48,8 @@ public class StripePaymentService : IPaymentService
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ITaxService taxService,
|
||||
ISubscriberService subscriberService)
|
||||
ISubscriberService subscriberService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_transactionRepository = transactionRepository;
|
||||
_logger = logger;
|
||||
@ -56,6 +59,7 @@ public class StripePaymentService : IPaymentService
|
||||
_featureService = featureService;
|
||||
_taxService = taxService;
|
||||
_subscriberService = subscriberService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
@ -297,7 +301,7 @@ public class StripePaymentService : IPaymentService
|
||||
OrganizationSponsorship sponsorship,
|
||||
bool applySponsorship)
|
||||
{
|
||||
var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType);
|
||||
var existingPlan = await _pricingClient.GetPlanOrThrow(org.PlanType);
|
||||
var sponsoredPlan = sponsorship?.PlanSponsorshipType != null ?
|
||||
Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) :
|
||||
null;
|
||||
@ -887,18 +891,21 @@ public class StripePaymentService : IPaymentService
|
||||
return paymentIntentClientSecret;
|
||||
}
|
||||
|
||||
public Task<string> AdjustSubscription(
|
||||
public async Task<string> AdjustSubscription(
|
||||
Organization organization,
|
||||
StaticStore.Plan updatedPlan,
|
||||
int newlyPurchasedPasswordManagerSeats,
|
||||
bool subscribedToSecretsManager,
|
||||
int? newlyPurchasedSecretsManagerSeats,
|
||||
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||
int newlyPurchasedAdditionalStorage) =>
|
||||
FinalizeSubscriptionChangeAsync(
|
||||
int newlyPurchasedAdditionalStorage)
|
||||
{
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
return await FinalizeSubscriptionChangeAsync(
|
||||
organization,
|
||||
new CompleteSubscriptionUpdate(
|
||||
organization,
|
||||
plan,
|
||||
new SubscriptionData
|
||||
{
|
||||
Plan = updatedPlan,
|
||||
@ -909,6 +916,7 @@ public class StripePaymentService : IPaymentService
|
||||
newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||
PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
|
||||
}), true);
|
||||
}
|
||||
|
||||
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats) =>
|
||||
FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats));
|
||||
@ -921,7 +929,7 @@ public class StripePaymentService : IPaymentService
|
||||
=> FinalizeSubscriptionChangeAsync(
|
||||
provider,
|
||||
new ProviderSubscriptionUpdate(
|
||||
plan.Type,
|
||||
plan,
|
||||
currentlySubscribedSeats,
|
||||
newlySubscribedSeats));
|
||||
|
||||
@ -1957,7 +1965,7 @@ public class StripePaymentService : IPaymentService
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models.StaticStore.Plans;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
@ -137,6 +138,7 @@ public static class StaticStore
|
||||
}
|
||||
|
||||
public static IDictionary<GlobalEquivalentDomainsType, IEnumerable<string>> GlobalDomains { get; set; }
|
||||
[Obsolete("Use PricingClient.ListPlans to retrieve all plans.")]
|
||||
public static IEnumerable<Plan> Plans { get; }
|
||||
public static IEnumerable<SponsoredPlan> SponsoredPlans { get; set; } = new[]
|
||||
{
|
||||
@ -147,10 +149,11 @@ public static class StaticStore
|
||||
SponsoringProductTierType = ProductTierType.Enterprise,
|
||||
StripePlanId = "2021-family-for-enterprise-annually",
|
||||
UsersCanSponsor = (OrganizationUserOrganizationDetails org) =>
|
||||
GetPlan(org.PlanType).ProductTier == ProductTierType.Enterprise,
|
||||
org.PlanType.GetProductTier() == ProductTierType.Enterprise,
|
||||
}
|
||||
};
|
||||
|
||||
[Obsolete("Use PricingClient.GetPlan to retrieve a plan.")]
|
||||
public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
|
||||
|
||||
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>
|
||||
@ -167,6 +170,7 @@ public static class StaticStore
|
||||
/// </returns>
|
||||
public static bool IsAddonSubscriptionItem(string stripePlanId)
|
||||
{
|
||||
// TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-16844
|
||||
return Plans.Any(p =>
|
||||
p.PasswordManager.StripeStoragePlanId == stripePlanId ||
|
||||
(p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId));
|
||||
|
Reference in New Issue
Block a user