mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 17:45:21 -06:00
[PM-24067] Check for unverified bank account in free trial / inactive subscription warning (#6117)
* [NO LOGIC] Move query to core * Check for unverified bank account in free trial and inactive subscription warnings * Run dotnet format * fix test * Run dotnet format * Remove errant file
This commit is contained in:
parent
988b994624
commit
2d1f914eae
@ -3,10 +3,10 @@ using System.Diagnostics;
|
|||||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
using Bit.Api.Billing.Models.Requests;
|
using Bit.Api.Billing.Models.Requests;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Api.Billing.Queries.Organizations;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Organizations.Models;
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
|
using Bit.Core.Billing.Organizations.Queries;
|
||||||
using Bit.Core.Billing.Organizations.Services;
|
using Bit.Core.Billing.Organizations.Services;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
@ -28,7 +28,7 @@ public class OrganizationBillingController(
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationWarningsQuery organizationWarningsQuery,
|
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
ISubscriberService subscriberService,
|
ISubscriberService subscriberService,
|
||||||
@ -363,7 +363,7 @@ public class OrganizationBillingController(
|
|||||||
public async Task<IResult> GetWarningsAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetWarningsAsync([FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* We'll keep these available at the User level, because we're hiding any pertinent information and
|
* We'll keep these available at the User level because we're hiding any pertinent information, and
|
||||||
* we want to throw as few errors as possible since these are not core features.
|
* we want to throw as few errors as possible since these are not core features.
|
||||||
*/
|
*/
|
||||||
if (!await currentContext.OrganizationUser(organizationId))
|
if (!await currentContext.OrganizationUser(organizationId))
|
||||||
@ -378,9 +378,9 @@ public class OrganizationBillingController(
|
|||||||
return Error.NotFound();
|
return Error.NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await organizationWarningsQuery.Run(organization);
|
var warnings = await getOrganizationWarningsQuery.Run(organization);
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(warnings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
using Bit.Api.Billing.Queries.Organizations;
|
|
||||||
|
|
||||||
namespace Bit.Api.Billing;
|
|
||||||
|
|
||||||
public static class Registrations
|
|
||||||
{
|
|
||||||
public static void AddBillingQueries(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddTransient<IOrganizationWarningsQuery, OrganizationWarningsQuery>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -27,7 +27,6 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
|||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||||
using Bit.Api.Billing;
|
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Auth.Identity.TokenProviders;
|
using Bit.Core.Auth.Identity.TokenProviders;
|
||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
@ -184,7 +183,6 @@ public class Startup
|
|||||||
services.AddImportServices();
|
services.AddImportServices();
|
||||||
services.AddPhishingDomainServices(globalSettings);
|
services.AddPhishingDomainServices(globalSettings);
|
||||||
|
|
||||||
services.AddBillingQueries();
|
|
||||||
services.AddSendServices();
|
services.AddSendServices();
|
||||||
|
|
||||||
// Authorization Handlers
|
// Authorization Handlers
|
||||||
|
|||||||
@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddTransient<IPreviewTaxAmountCommand, PreviewTaxAmountCommand>();
|
services.AddTransient<IPreviewTaxAmountCommand, PreviewTaxAmountCommand>();
|
||||||
services.AddPaymentOperations();
|
services.AddPaymentOperations();
|
||||||
services.AddOrganizationLicenseCommandsQueries();
|
services.AddOrganizationLicenseCommandsQueries();
|
||||||
|
services.AddTransient<IGetOrganizationWarningsQuery, GetOrganizationWarningsQuery>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)
|
private static void AddOrganizationLicenseCommandsQueries(this IServiceCollection services)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
#nullable enable
|
namespace Bit.Core.Billing.Organizations.Models;
|
||||||
namespace Bit.Api.Billing.Models.Responses.Organizations;
|
|
||||||
|
|
||||||
public record OrganizationWarningsResponse
|
public record OrganizationWarnings
|
||||||
{
|
{
|
||||||
public FreeTrialWarning? FreeTrial { get; set; }
|
public FreeTrialWarning? FreeTrial { get; set; }
|
||||||
public InactiveSubscriptionWarning? InactiveSubscription { get; set; }
|
public InactiveSubscriptionWarning? InactiveSubscription { get; set; }
|
||||||
@ -1,42 +1,44 @@
|
|||||||
// ReSharper disable InconsistentNaming
|
// ReSharper disable InconsistentNaming
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using Bit.Api.Billing.Models.Responses.Organizations;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Extensions;
|
||||||
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using static Bit.Core.Billing.Utilities;
|
using FreeTrialWarning = Bit.Core.Billing.Organizations.Models.OrganizationWarnings.FreeTrialWarning;
|
||||||
using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning;
|
|
||||||
using InactiveSubscriptionWarning =
|
using InactiveSubscriptionWarning =
|
||||||
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning;
|
Bit.Core.Billing.Organizations.Models.OrganizationWarnings.InactiveSubscriptionWarning;
|
||||||
using ResellerRenewalWarning =
|
using ResellerRenewalWarning =
|
||||||
Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning;
|
Bit.Core.Billing.Organizations.Models.OrganizationWarnings.ResellerRenewalWarning;
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Queries.Organizations;
|
namespace Bit.Core.Billing.Organizations.Queries;
|
||||||
|
|
||||||
public interface IOrganizationWarningsQuery
|
using static StripeConstants;
|
||||||
|
|
||||||
|
public interface IGetOrganizationWarningsQuery
|
||||||
{
|
{
|
||||||
Task<OrganizationWarningsResponse> Run(
|
Task<OrganizationWarnings> Run(
|
||||||
Organization organization);
|
Organization organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrganizationWarningsQuery(
|
public class GetOrganizationWarningsQuery(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IOrganizationWarningsQuery
|
ISubscriberService subscriberService) : IGetOrganizationWarningsQuery
|
||||||
{
|
{
|
||||||
public async Task<OrganizationWarningsResponse> Run(
|
public async Task<OrganizationWarnings> Run(
|
||||||
Organization organization)
|
Organization organization)
|
||||||
{
|
{
|
||||||
var response = new OrganizationWarningsResponse();
|
var response = new OrganizationWarnings();
|
||||||
|
|
||||||
var subscription =
|
var subscription =
|
||||||
await subscriberService.GetSubscription(organization,
|
await subscriberService.GetSubscription(organization,
|
||||||
@ -69,7 +71,7 @@ public class OrganizationWarningsQuery(
|
|||||||
|
|
||||||
if (subscription is not
|
if (subscription is not
|
||||||
{
|
{
|
||||||
Status: StripeConstants.SubscriptionStatus.Trialing,
|
Status: SubscriptionStatus.Trialing,
|
||||||
TrialEnd: not null,
|
TrialEnd: not null,
|
||||||
Customer: not null
|
Customer: not null
|
||||||
})
|
})
|
||||||
@ -79,10 +81,13 @@ public class OrganizationWarningsQuery(
|
|||||||
|
|
||||||
var customer = subscription.Customer;
|
var customer = subscription.Customer;
|
||||||
|
|
||||||
|
var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization);
|
||||||
|
|
||||||
var hasPaymentMethod =
|
var hasPaymentMethod =
|
||||||
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||||
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
|
!string.IsNullOrEmpty(customer.DefaultSourceId) ||
|
||||||
customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId);
|
hasUnverifiedBankAccount ||
|
||||||
|
customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
|
||||||
|
|
||||||
if (hasPaymentMethod)
|
if (hasPaymentMethod)
|
||||||
{
|
{
|
||||||
@ -101,49 +106,58 @@ public class OrganizationWarningsQuery(
|
|||||||
Provider? provider,
|
Provider? provider,
|
||||||
Subscription subscription)
|
Subscription subscription)
|
||||||
{
|
{
|
||||||
if (organization.Enabled && subscription.Status is StripeConstants.SubscriptionStatus.Trialing)
|
var isOrganizationOwner = await currentContext.OrganizationOwner(organization.Id);
|
||||||
{
|
|
||||||
var isStripeCustomerWithoutPayment =
|
|
||||||
subscription.Customer.InvoiceSettings.DefaultPaymentMethodId is null;
|
|
||||||
var isBraintreeCustomer =
|
|
||||||
subscription.Customer.Metadata.ContainsKey(BraintreeCustomerIdKey);
|
|
||||||
var hasNoPaymentMethod = isStripeCustomerWithoutPayment && !isBraintreeCustomer;
|
|
||||||
|
|
||||||
if (hasNoPaymentMethod && await currentContext.OrganizationOwner(organization.Id))
|
switch (organization.Enabled)
|
||||||
{
|
|
||||||
return new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organization.Enabled ||
|
|
||||||
subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid
|
|
||||||
and not StripeConstants.SubscriptionStatus.Canceled)
|
|
||||||
{
|
{
|
||||||
return null;
|
// Member of an enabled, trialing organization.
|
||||||
}
|
case true when subscription.Status is SubscriptionStatus.Trialing:
|
||||||
|
|
||||||
if (provider != null)
|
|
||||||
{
|
|
||||||
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await currentContext.OrganizationOwner(organization.Id))
|
|
||||||
{
|
|
||||||
return subscription.Status switch
|
|
||||||
{
|
|
||||||
StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
|
||||||
{
|
{
|
||||||
Resolution = "add_payment_method"
|
var hasUnverifiedBankAccount = await HasUnverifiedBankAccount(organization);
|
||||||
},
|
|
||||||
StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
|
||||||
{
|
|
||||||
Resolution = "resubscribe"
|
|
||||||
},
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
var hasPaymentMethod =
|
||||||
|
!string.IsNullOrEmpty(subscription.Customer.InvoiceSettings.DefaultPaymentMethodId) ||
|
||||||
|
!string.IsNullOrEmpty(subscription.Customer.DefaultSourceId) ||
|
||||||
|
hasUnverifiedBankAccount ||
|
||||||
|
subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
|
||||||
|
|
||||||
|
// If this member is the owner and there's no payment method on file, ask them to add one.
|
||||||
|
return isOrganizationOwner && !hasPaymentMethod
|
||||||
|
? new InactiveSubscriptionWarning { Resolution = "add_payment_method_optional_trial" }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
// Member of disabled and unpaid or canceled organization.
|
||||||
|
case false when subscription.Status is SubscriptionStatus.Unpaid or SubscriptionStatus.Canceled:
|
||||||
|
{
|
||||||
|
// If the organization is managed by a provider, return a warning asking them to contact the provider.
|
||||||
|
if (provider != null)
|
||||||
|
{
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_provider" };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If the organization is not managed by a provider and this user is the owner, return an action warning based
|
||||||
|
on the subscription status. */
|
||||||
|
if (isOrganizationOwner)
|
||||||
|
{
|
||||||
|
return subscription.Status switch
|
||||||
|
{
|
||||||
|
SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "add_payment_method"
|
||||||
|
},
|
||||||
|
SubscriptionStatus.Canceled => new InactiveSubscriptionWarning
|
||||||
|
{
|
||||||
|
Resolution = "resubscribe"
|
||||||
|
},
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, this member is not the owner, and we need to ask them to contact the owner.
|
||||||
|
return new InactiveSubscriptionWarning { Resolution = "contact_owner" };
|
||||||
|
}
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarning(
|
private async Task<ResellerRenewalWarning?> GetResellerRenewalWarning(
|
||||||
@ -158,7 +172,7 @@ public class OrganizationWarningsQuery(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice)
|
if (subscription.CollectionMethod != CollectionMethod.SendInvoice)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -168,8 +182,8 @@ public class OrganizationWarningsQuery(
|
|||||||
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
// ReSharper disable once ConvertIfStatementToSwitchStatement
|
||||||
if (subscription is
|
if (subscription is
|
||||||
{
|
{
|
||||||
Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active,
|
Status: SubscriptionStatus.Trialing or SubscriptionStatus.Active,
|
||||||
LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid }
|
LatestInvoice: null or { Status: InvoiceStatus.Paid }
|
||||||
} && (subscription.CurrentPeriodEnd - now).TotalDays <= 14)
|
} && (subscription.CurrentPeriodEnd - now).TotalDays <= 14)
|
||||||
{
|
{
|
||||||
return new ResellerRenewalWarning
|
return new ResellerRenewalWarning
|
||||||
@ -184,8 +198,8 @@ public class OrganizationWarningsQuery(
|
|||||||
|
|
||||||
if (subscription is
|
if (subscription is
|
||||||
{
|
{
|
||||||
Status: StripeConstants.SubscriptionStatus.Active,
|
Status: SubscriptionStatus.Active,
|
||||||
LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null }
|
LatestInvoice: { Status: InvoiceStatus.Open, DueDate: not null }
|
||||||
} && subscription.LatestInvoice.DueDate > now)
|
} && subscription.LatestInvoice.DueDate > now)
|
||||||
{
|
{
|
||||||
return new ResellerRenewalWarning
|
return new ResellerRenewalWarning
|
||||||
@ -200,7 +214,7 @@ public class OrganizationWarningsQuery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReSharper disable once InvertIf
|
// ReSharper disable once InvertIf
|
||||||
if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue)
|
if (subscription.Status == SubscriptionStatus.PastDue)
|
||||||
{
|
{
|
||||||
var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
|
var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions
|
||||||
{
|
{
|
||||||
@ -226,4 +240,22 @@ public class OrganizationWarningsQuery(
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> HasUnverifiedBankAccount(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
var setupIntentId = await setupIntentCache.Get(organization.Id);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(setupIntentId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||||
|
{
|
||||||
|
Expand = ["payment_method"]
|
||||||
|
});
|
||||||
|
|
||||||
|
return setupIntent.IsUnverifiedBankAccount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -14,7 +14,7 @@ using NSubstitute;
|
|||||||
using Xunit;
|
using Xunit;
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses;
|
namespace Bit.Core.Test.Billing.Organizations.Commands;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class UpdateOrganizationLicenseCommandTests
|
public class UpdateOrganizationLicenseCommandTests
|
||||||
@ -18,7 +18,7 @@ using NSubstitute.ReturnsExtensions;
|
|||||||
using Stripe;
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses;
|
namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||||
|
|
||||||
[SubscriptionInfoCustomize]
|
[SubscriptionInfoCustomize]
|
||||||
[OrganizationLicenseCustomize]
|
[OrganizationLicenseCustomize]
|
||||||
@ -1,12 +1,13 @@
|
|||||||
using Bit.Api.Billing.Queries.Organizations;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Organizations.Queries;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
@ -15,17 +16,17 @@ using Stripe;
|
|||||||
using Stripe.TestHelpers;
|
using Stripe.TestHelpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Api.Test.Billing.Queries.Organizations;
|
namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class OrganizationWarningsQueryTests
|
public class GetOrganizationWarningsQueryTests
|
||||||
{
|
{
|
||||||
private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"];
|
private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"];
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_NoSubscription_NoWarnings(
|
public async Task Run_NoSubscription_NoWarnings(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ISubscriberService>()
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
@ -46,7 +47,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_FreeTrialWarning(
|
public async Task Run_Has_FreeTrialWarning(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -70,6 +71,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
});
|
});
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
|
||||||
|
|
||||||
var response = await sutProvider.Sut.Run(organization);
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
@ -79,10 +81,90 @@ public class OrganizationWarningsQueryTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_FreeTrialWarning_WithUnverifiedBankAccount_NoWarning(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
const string setupIntentId = "setup_intent_id";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Trialing,
|
||||||
|
TrialEnd = now.AddDays(7),
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
},
|
||||||
|
TestClock = new TestClock
|
||||||
|
{
|
||||||
|
FrozenTime = now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().EditSubscription(organization.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns(setupIntentId);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(
|
||||||
|
options => options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||||
|
{
|
||||||
|
Status = "requires_action",
|
||||||
|
NextAction = new SetupIntentNextAction
|
||||||
|
{
|
||||||
|
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
|
||||||
|
},
|
||||||
|
PaymentMethod = new PaymentMethod
|
||||||
|
{
|
||||||
|
UsBankAccount = new PaymentMethodUsBankAccount()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.Null(response.FreeTrial);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethodOptionalTrial(
|
||||||
|
Organization organization,
|
||||||
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
|
{
|
||||||
|
organization.Enabled = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ISubscriberService>()
|
||||||
|
.GetSubscription(organization, Arg.Is<SubscriptionGetOptions>(options =>
|
||||||
|
options.Expand.SequenceEqual(_requiredExpansions)
|
||||||
|
))
|
||||||
|
.Returns(new Subscription
|
||||||
|
{
|
||||||
|
Status = StripeConstants.SubscriptionStatus.Trialing,
|
||||||
|
Customer = new Customer
|
||||||
|
{
|
||||||
|
InvoiceSettings = new CustomerInvoiceSettings(),
|
||||||
|
Metadata = new Dictionary<string, string>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<ISetupIntentCache>().Get(organization.Id).Returns((string?)null);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Run(organization);
|
||||||
|
|
||||||
|
Assert.True(response is
|
||||||
|
{
|
||||||
|
InactiveSubscription.Resolution: "add_payment_method_optional_trial"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider(
|
public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Enabled = false;
|
organization.Enabled = false;
|
||||||
|
|
||||||
@ -109,7 +191,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod(
|
public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Enabled = false;
|
organization.Enabled = false;
|
||||||
|
|
||||||
@ -135,7 +217,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe(
|
public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Enabled = false;
|
organization.Enabled = false;
|
||||||
|
|
||||||
@ -161,7 +243,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner(
|
public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
organization.Enabled = false;
|
organization.Enabled = false;
|
||||||
|
|
||||||
@ -187,7 +269,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_ResellerRenewalWarning_Upcoming(
|
public async Task Run_Has_ResellerRenewalWarning_Upcoming(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -225,7 +307,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_ResellerRenewalWarning_Issued(
|
public async Task Run_Has_ResellerRenewalWarning_Issued(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -269,7 +351,7 @@ public class OrganizationWarningsQueryTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Run_Has_ResellerRenewalWarning_PastDue(
|
public async Task Run_Has_ResellerRenewalWarning_PastDue(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
SutProvider<OrganizationWarningsQuery> sutProvider)
|
SutProvider<GetOrganizationWarningsQuery> sutProvider)
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
|||||||
using Bit.Test.Common.Helpers;
|
using Bit.Test.Common.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Bit.Core.Test.Billing.OrganizationFeatures.OrganizationLicenses;
|
namespace Bit.Core.Test.Billing.Organizations.Queries;
|
||||||
|
|
||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class GetSelfHostedOrganizationLicenseQueryTests
|
public class GetSelfHostedOrganizationLicenseQueryTests
|
||||||
Loading…
x
Reference in New Issue
Block a user