mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[PM-25379] Refactor org metadata (#6418)
* ignore serena * removing unused properties from org metadata * removing further properties that can already be fetched on the client side using available data * new vnext endpoint for org metadata plus caching metadata first pass including new feature flag # Conflicts: # src/Core/Constants.cs * [PM-25379] decided against cache and new query shouldn't use the service * pr feedback removing unneeded response model * run dotnet format
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -231,3 +231,4 @@ bitwarden_license/src/Sso/Sso.zip
|
||||
/identity.json
|
||||
/api.json
|
||||
/api.public.json
|
||||
.serena/
|
||||
|
||||
@@ -38,9 +38,7 @@ public class OrganizationBillingController(
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var response = OrganizationMetadataResponse.From(metadata);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
return TypedResults.Ok(metadata);
|
||||
}
|
||||
|
||||
[HttpGet("history")]
|
||||
|
||||
@@ -4,6 +4,7 @@ using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Api.Billing.Models.Requests.Subscriptions;
|
||||
using Bit.Api.Billing.Models.Requirements;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
@@ -25,6 +26,7 @@ public class OrganizationBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
IGetBillingAddressQuery getBillingAddressQuery,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetOrganizationMetadataQuery getOrganizationMetadataQuery,
|
||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IRestartSubscriptionCommand restartSubscriptionCommand,
|
||||
@@ -113,6 +115,23 @@ public class OrganizationBillingVNextController(
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("metadata")]
|
||||
[RequireFeature(FeatureFlagKeys.PM25379_UseNewOrganizationMetadataStructure)]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetMetadataAsync(
|
||||
[BindNever] Organization organization)
|
||||
{
|
||||
var metadata = await getOrganizationMetadataQuery.Run(organization);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
}
|
||||
|
||||
return TypedResults.Ok(metadata);
|
||||
}
|
||||
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("warnings")]
|
||||
[InjectOrganization]
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record OrganizationMetadataResponse(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsManaged,
|
||||
bool IsOnSecretsManagerStandalone,
|
||||
bool IsSubscriptionUnpaid,
|
||||
bool HasSubscription,
|
||||
bool HasOpenInvoice,
|
||||
bool IsSubscriptionCanceled,
|
||||
DateTime? InvoiceDueDate,
|
||||
DateTime? InvoiceCreatedDate,
|
||||
DateTime? SubPeriodEndDate,
|
||||
int OrganizationOccupiedSeats)
|
||||
{
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||
=> new(
|
||||
metadata.IsEligibleForSelfHost,
|
||||
metadata.IsManaged,
|
||||
metadata.IsOnSecretsManagerStandalone,
|
||||
metadata.IsSubscriptionUnpaid,
|
||||
metadata.HasSubscription,
|
||||
metadata.HasOpenInvoice,
|
||||
metadata.IsSubscriptionCanceled,
|
||||
metadata.InvoiceDueDate,
|
||||
metadata.InvoiceCreatedDate,
|
||||
metadata.SubPeriodEndDate,
|
||||
metadata.OrganizationOccupiedSeats);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddPaymentOperations();
|
||||
services.AddOrganizationLicenseCommandsQueries();
|
||||
services.AddPremiumCommands();
|
||||
services.AddTransient<IGetOrganizationMetadataQuery, GetOrganizationMetadataQuery>();
|
||||
services.AddTransient<IGetOrganizationWarningsQuery, GetOrganizationWarningsQuery>();
|
||||
services.AddTransient<IRestartSubscriptionCommand, RestartSubscriptionCommand>();
|
||||
services.AddTransient<IPreviewOrganizationTaxCommand, PreviewOrganizationTaxCommand>();
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
namespace Bit.Core.Billing.Organizations.Models;
|
||||
|
||||
public record OrganizationMetadata(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsManaged,
|
||||
bool IsOnSecretsManagerStandalone,
|
||||
bool IsSubscriptionUnpaid,
|
||||
bool HasSubscription,
|
||||
bool HasOpenInvoice,
|
||||
bool IsSubscriptionCanceled,
|
||||
DateTime? InvoiceDueDate,
|
||||
DateTime? InvoiceCreatedDate,
|
||||
DateTime? SubPeriodEndDate,
|
||||
int OrganizationOccupiedSeats)
|
||||
{
|
||||
public static OrganizationMetadata Default => new OrganizationMetadata(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Organizations.Queries;
|
||||
|
||||
public interface IGetOrganizationMetadataQuery
|
||||
{
|
||||
Task<OrganizationMetadata?> Run(Organization organization);
|
||||
}
|
||||
|
||||
public class GetOrganizationMetadataQuery(
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPricingClient pricingClient,
|
||||
ISubscriberService subscriberService) : IGetOrganizationMetadataQuery
|
||||
{
|
||||
public async Task<OrganizationMetadata?> Run(Organization organization)
|
||||
{
|
||||
if (organization == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
return OrganizationMetadata.Default;
|
||||
}
|
||||
|
||||
var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||
};
|
||||
}
|
||||
|
||||
var customer = await subscriberService.GetCustomer(organization,
|
||||
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||
};
|
||||
}
|
||||
|
||||
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
return new OrganizationMetadata(
|
||||
isOnSecretsManagerStandalone,
|
||||
orgOccupiedSeats.Total);
|
||||
}
|
||||
|
||||
private async Task<bool> IsOnSecretsManagerStandalone(
|
||||
Organization organization,
|
||||
Customer? customer,
|
||||
Subscription? subscription)
|
||||
{
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
|
||||
if (!plan.SupportsSecretsManager)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasCoupon = customer.Discount?.Coupon?.Id == StripeConstants.CouponIDs.SecretsManagerStandalone;
|
||||
|
||||
if (!hasCoupon)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var subscriptionProductIds = subscription.Items.Data.Select(item => item.Plan.ProductId);
|
||||
|
||||
var couponAppliesTo = customer.Discount?.Coupon?.AppliesTo?.Products;
|
||||
|
||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||
}
|
||||
}
|
||||
@@ -74,16 +74,12 @@ public class OrganizationBillingService(
|
||||
return OrganizationMetadata.Default;
|
||||
}
|
||||
|
||||
var isEligibleForSelfHost = await IsEligibleForSelfHostAsync(organization);
|
||||
|
||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||
var orgOccupiedSeats = await organizationRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||
IsManaged = isManaged,
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||
};
|
||||
}
|
||||
@@ -97,28 +93,14 @@ public class OrganizationBillingService(
|
||||
{
|
||||
return OrganizationMetadata.Default with
|
||||
{
|
||||
IsEligibleForSelfHost = isEligibleForSelfHost,
|
||||
IsManaged = isManaged
|
||||
OrganizationOccupiedSeats = orgOccupiedSeats.Total
|
||||
};
|
||||
}
|
||||
|
||||
var isOnSecretsManagerStandalone = await IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
var invoice = !string.IsNullOrEmpty(subscription.LatestInvoiceId)
|
||||
? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions())
|
||||
: null;
|
||||
|
||||
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,
|
||||
orgOccupiedSeats.Total);
|
||||
}
|
||||
|
||||
@@ -536,16 +518,6 @@ public class OrganizationBillingService(
|
||||
return customer;
|
||||
}
|
||||
|
||||
private async Task<bool> IsEligibleForSelfHostAsync(
|
||||
Organization organization)
|
||||
{
|
||||
var plans = await pricingClient.ListPlans();
|
||||
|
||||
var eligibleSelfHostPlans = plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||
|
||||
return eligibleSelfHostPlans.Contains(organization.PlanType);
|
||||
}
|
||||
|
||||
private async Task<bool> IsOnSecretsManagerStandalone(
|
||||
Organization organization,
|
||||
Customer? customer,
|
||||
|
||||
@@ -179,6 +179,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM21821_ProviderPortalTakeover = "pm-21821-provider-portal-takeover";
|
||||
public const string PM22415_TaxIDWarnings = "pm-22415-tax-id-warnings";
|
||||
public const string PM25379_UseNewOrganizationMetadataStructure = "pm-25379-use-new-organization-metadata-structure";
|
||||
public const string PM24996ImplementUpgradeFromFreeDialog = "pm-24996-implement-upgrade-from-free-dialog";
|
||||
public const string PM24032_NewNavigationPremiumUpgradeButton = "pm-24032-new-navigation-premium-upgrade-button";
|
||||
public const string PM23713_PremiumBadgeOpensNewPremiumUpgradeDialog = "pm-23713-premium-badge-opens-new-premium-upgrade-dialog";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
@@ -53,19 +52,16 @@ public class OrganizationBillingControllerTests
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0));
|
||||
.Returns(new OrganizationMetadata(true, 10));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
Assert.IsType<Ok<OrganizationMetadataResponse>>(result);
|
||||
Assert.IsType<Ok<OrganizationMetadata>>(result);
|
||||
|
||||
var response = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||
var response = ((Ok<OrganizationMetadata>)result).Value;
|
||||
|
||||
Assert.True(response.IsEligibleForSelfHost);
|
||||
Assert.True(response.IsManaged);
|
||||
Assert.True(response.IsOnSecretsManagerStandalone);
|
||||
Assert.True(response.IsSubscriptionUnpaid);
|
||||
Assert.True(response.HasSubscription);
|
||||
Assert.Equal(10, response.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class GetOrganizationMetadataQueryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NullOrganization_ReturnsNull(
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
var result = await sutProvider.Sut.Run(null);
|
||||
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_SelfHosted_ReturnsDefault(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.Equal(OrganizationMetadata.Default, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NoGatewaySubscriptionId_ReturnsDefaultWithOccupiedSeats(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = null;
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 10, Sponsored = 0 });
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(10, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NullCustomer_ReturnsDefaultWithOccupiedSeats(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 5, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.ReturnsNull();
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(5, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_NullSubscription_ReturnsDefaultWithOccupiedSeats(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
|
||||
var customer = new Customer();
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 7, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.ReturnsNull();
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(7, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_WithSecretsManagerStandaloneCoupon_ReturnsMetadataWithFlag(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var productId = "product_123";
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = productId
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 15, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(15, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_WithoutSecretsManagerStandaloneCoupon_ReturnsMetadataWithoutFlag(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.TeamsAnnually;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = null
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = "product_123"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 20, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(20, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_CouponDoesNotApplyToSubscriptionProducts_ReturnsFalseForStandaloneFlag(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = ["different_product_id"]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = "product_123"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 12, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(12, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Run_PlanDoesNotSupportSecretsManager_ReturnsFalseForStandaloneFlag(
|
||||
Organization organization,
|
||||
SutProvider<GetOrganizationMetadataQuery> sutProvider)
|
||||
{
|
||||
organization.GatewaySubscriptionId = "sub_123";
|
||||
organization.PlanType = PlanType.FamiliesAnnually;
|
||||
|
||||
var productId = "product_123";
|
||||
var customer = new Customer
|
||||
{
|
||||
Discount = new Discount
|
||||
{
|
||||
Coupon = new Coupon
|
||||
{
|
||||
Id = StripeConstants.CouponIDs.SecretsManagerStandalone,
|
||||
AppliesTo = new CouponAppliesTo
|
||||
{
|
||||
Products = [productId]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
Plan = new Plan
|
||||
{
|
||||
ProductId = productId
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IGlobalSettings>().SelfHosted.Returns(false);
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 8, Sponsored = 0 });
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetCustomer(organization, Arg.Is<CustomerGetOptions>(options =>
|
||||
options.Expand.Contains("discount.coupon.applies_to")))
|
||||
.Returns(customer);
|
||||
|
||||
sutProvider.GetDependency<ISubscriberService>()
|
||||
.GetSubscription(organization)
|
||||
.Returns(subscription);
|
||||
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
var result = await sutProvider.Sut.Run(organization);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.False(result.IsOnSecretsManagerStandalone);
|
||||
Assert.Equal(8, result.OrganizationOccupiedSeats);
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,10 @@ public class OrganizationBillingServiceTests
|
||||
sutProvider.GetDependency<IPricingClient>().GetPlanOrThrow(organization.PlanType)
|
||||
.Returns(StaticStore.GetPlan(organization.PlanType));
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id)
|
||||
.Returns(new OrganizationSeatCounts { Users = 1, Sponsored = 0 });
|
||||
|
||||
var subscriberService = sutProvider.GetDependency<ISubscriberService>();
|
||||
|
||||
// Set up subscriber service to return null for customer
|
||||
@@ -110,13 +114,7 @@ public class OrganizationBillingServiceTests
|
||||
|
||||
Assert.NotNull(metadata);
|
||||
Assert.False(metadata!.IsOnSecretsManagerStandalone);
|
||||
Assert.False(metadata.HasSubscription);
|
||||
Assert.False(metadata.IsSubscriptionUnpaid);
|
||||
Assert.False(metadata.HasOpenInvoice);
|
||||
Assert.False(metadata.IsSubscriptionCanceled);
|
||||
Assert.Null(metadata.InvoiceDueDate);
|
||||
Assert.Null(metadata.InvoiceCreatedDate);
|
||||
Assert.Null(metadata.SubPeriodEndDate);
|
||||
Assert.Equal(1, metadata.OrganizationOccupiedSeats);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
Reference in New Issue
Block a user