mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
2b43cde99b
* Refactored GET provider subscription Refactoring this endpoint and its associated tests in preparation for the addition of more endpoints that share similar patterns * Replaced StripePaymentService call in AccountsController, OrganizationsController This was made in error during a previous PR. Since this is not related to Consolidated Billing, we want to try not to include it in these changes. * Removing GetPaymentInformation call from ProviderBillingService This method is a good call for the SubscriberService as we'll want to extend the functionality to all subscriber types * Refactored GetTaxInformation to use Billing owned DTO * Add UpdateTaxInformation to SubscriberService * Added GetTaxInformation and UpdateTaxInformation endpoints to ProviderBillingController * Added controller to manage creation of Stripe SetupIntents With the deprecation of the Sources API, we need to move the bank account creation process to using SetupIntents. This controller brings both the creation of "card" and "us_bank_account" SetupIntents under billing management. * Added UpdatePaymentMethod method to SubscriberService This method utilizes the SetupIntents created by the StripeController from the previous commit when a customer adds a card or us_bank_account payment method (Stripe). We need to cache the most recent SetupIntent for the subscriber so that we know which PaymentMethod is their most recent even when it hasn't been confirmed yet. * Refactored GetPaymentMethod to use billing owned DTO and check setup intents * Added GetPaymentMethod and UpdatePaymentMethod endpoints to ProviderBillingController * Re-added GetPaymentInformation endpoint to consolidate API calls on the payment method page * Added VerifyBankAccount endpoint to ProviderBillingController in order to finalize bank account payment methods * Updated BitPayInvoiceRequestModel to support providers * run dotnet format * Conner's feedback * Run dotnet format'
388 lines
14 KiB
C#
388 lines
14 KiB
C#
using Bit.Api.Billing.Controllers;
|
|
using Bit.Api.Billing.Models.Requests;
|
|
using Bit.Api.Billing.Models.Responses;
|
|
using Bit.Core;
|
|
using Bit.Core.AdminConsole.Entities.Provider;
|
|
using Bit.Core.AdminConsole.Enums.Provider;
|
|
using Bit.Core.AdminConsole.Repositories;
|
|
using Bit.Core.Billing.Constants;
|
|
using Bit.Core.Billing.Models;
|
|
using Bit.Core.Billing.Services;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Test.Common.AutoFixture;
|
|
using Bit.Test.Common.AutoFixture.Attributes;
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using NSubstitute;
|
|
using NSubstitute.ReturnsExtensions;
|
|
using Stripe;
|
|
using Xunit;
|
|
|
|
namespace Bit.Api.Test.Billing.Controllers;
|
|
|
|
[ControllerCustomize(typeof(ProviderBillingController))]
|
|
[SutProviderCustomize]
|
|
public class ProviderBillingControllerTests
|
|
{
|
|
#region GetSubscriptionAsync
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_FFDisabled_NotFound(
|
|
Guid providerId,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
.Returns(false);
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_NullProvider_NotFound(
|
|
Guid providerId,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
.Returns(true);
|
|
|
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(providerId).ReturnsNull();
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(providerId);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_NotProviderAdmin_Unauthorized(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
.Returns(true);
|
|
|
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
|
.Returns(false);
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<UnauthorizedHttpResult>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_ProviderNotBillable_Unauthorized(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
.Returns(true);
|
|
|
|
provider.Type = ProviderType.Reseller;
|
|
provider.Status = ProviderStatusType.Created;
|
|
|
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
|
.Returns(false);
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<UnauthorizedHttpResult>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_NullConsolidatedBillingSubscription_NotFound(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<IProviderBillingService>().GetConsolidatedBillingSubscription(provider).ReturnsNull();
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetSubscriptionAsync_Ok(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
var configuredProviderPlans = new List<ConfiguredProviderPlanDTO>
|
|
{
|
|
new (Guid.NewGuid(), provider.Id, PlanType.TeamsMonthly, 50, 10, 30),
|
|
new (Guid.NewGuid(), provider.Id , PlanType.EnterpriseMonthly, 100, 0, 90)
|
|
};
|
|
|
|
var subscription = new Subscription
|
|
{
|
|
Status = "active",
|
|
CurrentPeriodEnd = new DateTime(2025, 1, 1),
|
|
Customer = new Customer { Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } } }
|
|
};
|
|
|
|
var consolidatedBillingSubscription = new ConsolidatedBillingSubscriptionDTO(
|
|
configuredProviderPlans,
|
|
subscription);
|
|
|
|
sutProvider.GetDependency<IProviderBillingService>().GetConsolidatedBillingSubscription(provider)
|
|
.Returns(consolidatedBillingSubscription);
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<Ok<ConsolidatedBillingSubscriptionResponse>>(result);
|
|
|
|
var response = ((Ok<ConsolidatedBillingSubscriptionResponse>)result).Value;
|
|
|
|
Assert.Equal(response.Status, subscription.Status);
|
|
Assert.Equal(response.CurrentPeriodEndDate, subscription.CurrentPeriodEnd);
|
|
Assert.Equal(response.DiscountPercentage, subscription.Customer!.Discount!.Coupon!.PercentOff);
|
|
|
|
var teamsPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
|
var providerTeamsPlan = response.Plans.FirstOrDefault(plan => plan.PlanName == teamsPlan.Name);
|
|
Assert.NotNull(providerTeamsPlan);
|
|
Assert.Equal(50, providerTeamsPlan.SeatMinimum);
|
|
Assert.Equal(10, providerTeamsPlan.PurchasedSeats);
|
|
Assert.Equal(30, providerTeamsPlan.AssignedSeats);
|
|
Assert.Equal(60 * teamsPlan.PasswordManager.SeatPrice, providerTeamsPlan.Cost);
|
|
Assert.Equal("Monthly", providerTeamsPlan.Cadence);
|
|
|
|
var enterprisePlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
|
var providerEnterprisePlan = response.Plans.FirstOrDefault(plan => plan.PlanName == enterprisePlan.Name);
|
|
Assert.NotNull(providerEnterprisePlan);
|
|
Assert.Equal(100, providerEnterprisePlan.SeatMinimum);
|
|
Assert.Equal(0, providerEnterprisePlan.PurchasedSeats);
|
|
Assert.Equal(90, providerEnterprisePlan.AssignedSeats);
|
|
Assert.Equal(100 * enterprisePlan.PasswordManager.SeatPrice, providerEnterprisePlan.Cost);
|
|
Assert.Equal("Monthly", providerEnterprisePlan.Cadence);
|
|
}
|
|
#endregion
|
|
|
|
#region GetPaymentInformationAsync
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetPaymentInformation_PaymentInformationNull_NotFound(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetPaymentInformation(provider).ReturnsNull();
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetPaymentInformation_Ok(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
var maskedPaymentMethod = new MaskedPaymentMethodDTO(PaymentMethodType.Card, "VISA *1234", false);
|
|
|
|
var taxInformation =
|
|
new TaxInformationDTO("US", "12345", "123456789", "123 Example St.", null, "Example Town", "NY");
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetPaymentInformation(provider).Returns(new PaymentInformationDTO(
|
|
100,
|
|
maskedPaymentMethod,
|
|
taxInformation));
|
|
|
|
var result = await sutProvider.Sut.GetPaymentInformationAsync(provider.Id);
|
|
|
|
Assert.IsType<Ok<PaymentInformationResponse>>(result);
|
|
|
|
var response = ((Ok<PaymentInformationResponse>)result).Value;
|
|
|
|
Assert.Equal(100, response.AccountCredit);
|
|
Assert.Equal(maskedPaymentMethod.Description, response.PaymentMethod.Description);
|
|
Assert.Equal(taxInformation.TaxId, response.TaxInformation.TaxId);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetPaymentMethodAsync
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetPaymentMethod_PaymentMethodNull_NotFound(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetPaymentMethod(provider).ReturnsNull();
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetPaymentMethod_Ok(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetPaymentMethod(provider).Returns(new MaskedPaymentMethodDTO(
|
|
PaymentMethodType.Card, "Description", false));
|
|
|
|
var result = await sutProvider.Sut.GetPaymentMethodAsync(provider.Id);
|
|
|
|
Assert.IsType<Ok<MaskedPaymentMethodResponse>>(result);
|
|
|
|
var response = ((Ok<MaskedPaymentMethodResponse>)result).Value;
|
|
|
|
Assert.Equal(PaymentMethodType.Card, response.Type);
|
|
Assert.Equal("Description", response.Description);
|
|
Assert.False(response.NeedsVerification);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region GetTaxInformationAsync
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetTaxInformation_TaxInformationNull_NotFound(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetTaxInformation(provider).ReturnsNull();
|
|
|
|
var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id);
|
|
|
|
Assert.IsType<NotFound>(result);
|
|
}
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task GetTaxInformation_Ok(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
sutProvider.GetDependency<ISubscriberService>().GetTaxInformation(provider).Returns(new TaxInformationDTO(
|
|
"US",
|
|
"12345",
|
|
"123456789",
|
|
"123 Example St.",
|
|
null,
|
|
"Example Town",
|
|
"NY"));
|
|
|
|
var result = await sutProvider.Sut.GetTaxInformationAsync(provider.Id);
|
|
|
|
Assert.IsType<Ok<TaxInformationResponse>>(result);
|
|
|
|
var response = ((Ok<TaxInformationResponse>)result).Value;
|
|
|
|
Assert.Equal("US", response.Country);
|
|
Assert.Equal("12345", response.PostalCode);
|
|
Assert.Equal("123456789", response.TaxId);
|
|
Assert.Equal("123 Example St.", response.Line1);
|
|
Assert.Null(response.Line2);
|
|
Assert.Equal("Example Town", response.City);
|
|
Assert.Equal("NY", response.State);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UpdatePaymentMethodAsync
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task UpdatePaymentMethod_Ok(
|
|
Provider provider,
|
|
TokenizedPaymentMethodRequestBody requestBody,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
await sutProvider.Sut.UpdatePaymentMethodAsync(provider.Id, requestBody);
|
|
|
|
await sutProvider.GetDependency<ISubscriberService>().Received(1).UpdatePaymentMethod(
|
|
provider, Arg.Is<TokenizedPaymentMethodDTO>(
|
|
options => options.Type == requestBody.Type && options.Token == requestBody.Token));
|
|
|
|
await sutProvider.GetDependency<IStripeAdapter>().Received(1).SubscriptionUpdateAsync(
|
|
provider.GatewaySubscriptionId, Arg.Is<SubscriptionUpdateOptions>(
|
|
options => options.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region UpdateTaxInformationAsync
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task UpdateTaxInformation_Ok(
|
|
Provider provider,
|
|
TaxInformationRequestBody requestBody,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
|
|
|
await sutProvider.GetDependency<ISubscriberService>().Received(1).UpdateTaxInformation(
|
|
provider, Arg.Is<TaxInformationDTO>(
|
|
options =>
|
|
options.Country == requestBody.Country &&
|
|
options.PostalCode == requestBody.PostalCode &&
|
|
options.TaxId == requestBody.TaxId &&
|
|
options.Line1 == requestBody.Line1 &&
|
|
options.Line2 == requestBody.Line2 &&
|
|
options.City == requestBody.City &&
|
|
options.State == requestBody.State));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region VerifyBankAccount
|
|
|
|
[Theory, BitAutoData]
|
|
public async Task VerifyBankAccount_Ok(
|
|
Provider provider,
|
|
VerifyBankAccountRequestBody requestBody,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
ConfigureStableInputs(provider, sutProvider);
|
|
|
|
var result = await sutProvider.Sut.VerifyBankAccountAsync(provider.Id, requestBody);
|
|
|
|
Assert.IsType<Ok>(result);
|
|
|
|
await sutProvider.GetDependency<ISubscriberService>().Received(1).VerifyBankAccount(
|
|
provider,
|
|
(requestBody.Amount1, requestBody.Amount2));
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static void ConfigureStableInputs(
|
|
Provider provider,
|
|
SutProvider<ProviderBillingController> sutProvider)
|
|
{
|
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling)
|
|
.Returns(true);
|
|
|
|
provider.Type = ProviderType.Msp;
|
|
provider.Status = ProviderStatusType.Billable;
|
|
|
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
|
|
|
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
|
.Returns(true);
|
|
}
|
|
}
|