mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01:00
[AC-2959] ACH Direct Debit POC (#4703)
* Refactor: Rename some methods and models for consistency This commit contains no logic changes at all. It's entirely comprised of renames of existing models and methods to bring our codebase more in line with our app's functionality and terminology. * Add feature flag: AC-2476-deprecate-stripe-sources-api * Standardize error responses from applicable billing controllers During my work on CB, I found that just using the built-in TypedResults errors results in the client choking on the response because it's looking for the ErrroResponseModel. The new BaseBillingController provides Error utilities to return TypedResults wrapping that model so the client can process it. * Add feature flagged payment method endoints to OrganizationBillingController * Run dotnet format
This commit is contained in:
parent
20478949d8
commit
3c86ec6a35
@ -155,7 +155,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
DaysUntilDue = 30
|
||||
});
|
||||
|
||||
await _subscriberService.RemovePaymentMethod(organization);
|
||||
await _subscriberService.RemovePaymentSource(organization);
|
||||
}
|
||||
|
||||
await _mailService.SendProviderUpdatePaymentMethod(
|
||||
|
@ -173,7 +173,7 @@ public class RemoveOrganizationFromProviderCommandTests
|
||||
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
|
||||
options.DaysUntilDue == 30));
|
||||
|
||||
await sutProvider.GetDependency<ISubscriberService>().Received(1).RemovePaymentMethod(organization);
|
||||
await sutProvider.GetDependency<ISubscriberService>().Received(1).RemovePaymentSource(organization);
|
||||
|
||||
await organizationRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == "a@example.com"));
|
||||
|
||||
|
30
src/Api/Billing/Controllers/BaseBillingController.cs
Normal file
30
src/Api/Billing/Controllers/BaseBillingController.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Bit.Core.Models.Api;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
public abstract class BaseBillingController : Controller
|
||||
{
|
||||
protected static class Error
|
||||
{
|
||||
public static BadRequest<ErrorResponseModel> BadRequest(Dictionary<string, IEnumerable<string>> errors) =>
|
||||
TypedResults.BadRequest(new ErrorResponseModel(errors));
|
||||
|
||||
public static BadRequest<ErrorResponseModel> BadRequest(string message) =>
|
||||
TypedResults.BadRequest(new ErrorResponseModel(message));
|
||||
|
||||
public static NotFound<ErrorResponseModel> NotFound() =>
|
||||
TypedResults.NotFound(new ErrorResponseModel("Resource not found."));
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> ServerError(string message = "Something went wrong with your request. Please contact support.") =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel(message),
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> Unauthorized() =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel("Unauthorized."),
|
||||
statusCode: StatusCodes.Status401Unauthorized);
|
||||
}
|
||||
}
|
@ -3,10 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
@ -15,23 +12,10 @@ public abstract class BaseProviderController(
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IProviderRepository providerRepository,
|
||||
IUserService userService) : Controller
|
||||
IUserService userService) : BaseBillingController
|
||||
{
|
||||
protected readonly IUserService UserService = userService;
|
||||
|
||||
protected static NotFound<ErrorResponseModel> NotFoundResponse() =>
|
||||
TypedResults.NotFound(new ErrorResponseModel("Resource not found."));
|
||||
|
||||
protected static JsonHttpResult<ErrorResponseModel> ServerErrorResponse(string errorMessage) =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel(errorMessage),
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
|
||||
protected static JsonHttpResult<ErrorResponseModel> UnauthorizedResponse() =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel("Unauthorized."),
|
||||
statusCode: StatusCodes.Status401Unauthorized);
|
||||
|
||||
protected Task<(Provider, IResult)> TryGetBillableProviderForAdminOperation(
|
||||
Guid providerId) => TryGetBillableProviderAsync(providerId, currentContext.ProviderProviderAdmin);
|
||||
|
||||
@ -48,7 +32,7 @@ public abstract class BaseProviderController(
|
||||
"Cannot run Consolidated Billing operation for provider ({ProviderID}) while feature flag is disabled",
|
||||
providerId);
|
||||
|
||||
return (null, NotFoundResponse());
|
||||
return (null, Error.NotFound());
|
||||
}
|
||||
|
||||
var provider = await providerRepository.GetByIdAsync(providerId);
|
||||
@ -59,7 +43,7 @@ public abstract class BaseProviderController(
|
||||
"Cannot find provider ({ProviderID}) for Consolidated Billing operation",
|
||||
providerId);
|
||||
|
||||
return (null, NotFoundResponse());
|
||||
return (null, Error.NotFound());
|
||||
}
|
||||
|
||||
if (!checkAuthorization(providerId))
|
||||
@ -70,7 +54,7 @@ public abstract class BaseProviderController(
|
||||
"User ({UserID}) is not authorized to perform Consolidated Billing operation for provider ({ProviderID})",
|
||||
user?.Id, providerId);
|
||||
|
||||
return (null, UnauthorizedResponse());
|
||||
return (null, Error.Unauthorized());
|
||||
}
|
||||
|
||||
if (!provider.IsBillable())
|
||||
@ -79,7 +63,7 @@ public abstract class BaseProviderController(
|
||||
"Cannot run Consolidated Billing operation for provider ({ProviderID}) that is not billable",
|
||||
providerId);
|
||||
|
||||
return (null, UnauthorizedResponse());
|
||||
return (null, Error.Unauthorized());
|
||||
}
|
||||
|
||||
if (provider.IsStripeEnabled())
|
||||
@ -91,6 +75,6 @@ public abstract class BaseProviderController(
|
||||
"Cannot run Consolidated Billing operation for provider ({ProviderID}) that is missing Stripe configuration",
|
||||
providerId);
|
||||
|
||||
return (null, ServerErrorResponse("Something went wrong with your request. Please contact support."));
|
||||
return (null, Error.ServerError());
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
@ -13,23 +15,25 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class OrganizationBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService) : Controller
|
||||
IPaymentService paymentService,
|
||||
ISubscriberService subscriberService) : BaseBillingController
|
||||
{
|
||||
[HttpGet("metadata")]
|
||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.AccessMembersTab(organizationId))
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var metadata = await organizationBillingService.GetMetadata(organizationId);
|
||||
|
||||
if (metadata == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var response = OrganizationMetadataResponse.From(metadata);
|
||||
@ -42,14 +46,14 @@ public class OrganizationBillingController(
|
||||
{
|
||||
if (!await currentContext.ViewBillingHistory(organizationId))
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var billingInfo = await paymentService.GetBillingHistoryAsync(organization);
|
||||
@ -63,14 +67,14 @@ public class OrganizationBillingController(
|
||||
{
|
||||
if (!await currentContext.ViewBillingHistory(organizationId))
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return TypedResults.NotFound();
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var billingInfo = await paymentService.GetBillingAsync(organization);
|
||||
@ -79,4 +83,147 @@ public class OrganizationBillingController(
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var paymentMethod = await subscriberService.GetPaymentMethod(organization);
|
||||
|
||||
var response = PaymentMethodResponse.From(paymentMethod);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
||||
|
||||
await subscriberService.UpdatePaymentSource(organization, tokenizedPaymentSource);
|
||||
|
||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
||||
|
||||
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
await subscriberService.VerifyBankAccount(organization, (requestBody.Amount1, requestBody.Amount2));
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var taxInformation = await subscriberService.GetTaxInformation(organization);
|
||||
|
||||
var response = TaxInformationResponse.From(taxInformation);
|
||||
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
|
||||
[HttpPut("tax-information")]
|
||||
public async Task<IResult> UpdateTaxInformationAsync(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] TaxInformationRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var taxInformation = requestBody.ToDomain();
|
||||
|
||||
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -63,7 +62,7 @@ public class ProviderBillingController(
|
||||
|
||||
if (reportContent == null)
|
||||
{
|
||||
return ServerErrorResponse("We had a problem generating your invoice CSV. Please contact support.");
|
||||
return Error.ServerError("We had a problem generating your invoice CSV. Please contact support.");
|
||||
}
|
||||
|
||||
return TypedResults.File(
|
||||
@ -113,8 +112,7 @@ public class ProviderBillingController(
|
||||
|
||||
if (requestBody is not { Country: not null, PostalCode: not null })
|
||||
{
|
||||
return TypedResults.BadRequest(
|
||||
new ErrorResponseModel("Country and postal code are required to update your tax information."));
|
||||
return Error.BadRequest("Country and postal code are required to update your tax information.");
|
||||
}
|
||||
|
||||
var taxInformation = new TaxInformation(
|
||||
|
@ -39,7 +39,7 @@ public class ProviderClientsController(
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return UnauthorizedResponse();
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organizationSignup = new OrganizationSignup
|
||||
@ -96,7 +96,7 @@ public class ProviderClientsController(
|
||||
|
||||
if (providerOrganization == null)
|
||||
{
|
||||
return NotFoundResponse();
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var clientOrganization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
@ -13,4 +14,13 @@ public class TaxInformationRequestBody
|
||||
public string Line2 { get; set; }
|
||||
public string City { get; set; }
|
||||
public string State { get; set; }
|
||||
|
||||
public TaxInformation ToDomain() => new(
|
||||
Country,
|
||||
PostalCode,
|
||||
TaxId,
|
||||
Line1,
|
||||
Line2,
|
||||
City,
|
||||
State);
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class TokenizedPaymentMethodRequestBody
|
||||
public class TokenizedPaymentSourceRequestBody
|
||||
{
|
||||
[Required]
|
||||
[EnumMatches<PaymentMethodType>(
|
||||
@ -13,6 +14,9 @@ public class TokenizedPaymentMethodRequestBody
|
||||
PaymentMethodType.PayPal,
|
||||
ErrorMessage = "'type' must be BankAccount, Card or PayPal")]
|
||||
public PaymentMethodType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
public TokenizedPaymentSource ToDomain() => new(Type, Token);
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class UpdatePaymentMethodRequestBody
|
||||
{
|
||||
[Required]
|
||||
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
|
||||
|
||||
[Required]
|
||||
public TaxInformationRequestBody TaxInformation { get; set; }
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record MaskedPaymentMethodResponse(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static MaskedPaymentMethodResponse From(MaskedPaymentMethodDTO maskedPaymentMethod)
|
||||
=> new(
|
||||
maskedPaymentMethod.Type,
|
||||
maskedPaymentMethod.Description,
|
||||
maskedPaymentMethod.NeedsVerification);
|
||||
}
|
@ -5,6 +5,6 @@ namespace Bit.Api.Billing.Models.Responses;
|
||||
public record OrganizationMetadataResponse(
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
{
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadataDTO metadataDTO)
|
||||
=> new(metadataDTO.IsOnSecretsManagerStandalone);
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||
=> new(metadata.IsOnSecretsManagerStandalone);
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentInformationResponse(
|
||||
long AccountCredit,
|
||||
MaskedPaymentMethodDTO PaymentMethod,
|
||||
TaxInformation TaxInformation)
|
||||
{
|
||||
public static PaymentInformationResponse From(PaymentInformationDTO paymentInformation) =>
|
||||
new(
|
||||
paymentInformation.AccountCredit,
|
||||
paymentInformation.PaymentMethod,
|
||||
paymentInformation.TaxInformation);
|
||||
}
|
17
src/Api/Billing/Models/Responses/PaymentMethodResponse.cs
Normal file
17
src/Api/Billing/Models/Responses/PaymentMethodResponse.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentMethodResponse(
|
||||
long AccountCredit,
|
||||
PaymentSource PaymentSource,
|
||||
string SubscriptionStatus,
|
||||
TaxInformation TaxInformation)
|
||||
{
|
||||
public static PaymentMethodResponse From(PaymentMethod paymentMethod) =>
|
||||
new(
|
||||
paymentMethod.AccountCredit,
|
||||
paymentMethod.PaymentSource,
|
||||
paymentMethod.SubscriptionStatus,
|
||||
paymentMethod.TaxInformation);
|
||||
}
|
16
src/Api/Billing/Models/Responses/PaymentSourceResponse.cs
Normal file
16
src/Api/Billing/Models/Responses/PaymentSourceResponse.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentSourceResponse(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static PaymentSourceResponse From(PaymentSource paymentMethod)
|
||||
=> new(
|
||||
paymentMethod.Type,
|
||||
paymentMethod.Description,
|
||||
paymentMethod.NeedsVerification);
|
||||
}
|
@ -12,7 +12,7 @@ public class BillingInfo
|
||||
{
|
||||
public BillingSource() { }
|
||||
|
||||
public BillingSource(PaymentMethod method)
|
||||
public BillingSource(Stripe.PaymentMethod method)
|
||||
{
|
||||
if (method.Card == null)
|
||||
{
|
||||
|
@ -1,8 +1,8 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record OrganizationMetadataDTO(
|
||||
public record OrganizationMetadata(
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
{
|
||||
public static OrganizationMetadataDTO Default() => new(
|
||||
public static OrganizationMetadata Default() => new(
|
||||
IsOnSecretsManagerStandalone: default);
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record PaymentInformationDTO(
|
||||
public record PaymentMethod(
|
||||
long AccountCredit,
|
||||
MaskedPaymentMethodDTO PaymentMethod,
|
||||
PaymentSource PaymentSource,
|
||||
string SubscriptionStatus,
|
||||
TaxInformation TaxInformation);
|
@ -3,12 +3,12 @@ using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record MaskedPaymentMethodDTO(
|
||||
public record PaymentSource(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static MaskedPaymentMethodDTO From(Stripe.Customer customer)
|
||||
public static PaymentSource From(Stripe.Customer customer)
|
||||
{
|
||||
var defaultPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod;
|
||||
|
||||
@ -25,7 +25,7 @@ public record MaskedPaymentMethodDTO(
|
||||
};
|
||||
}
|
||||
|
||||
public static MaskedPaymentMethodDTO From(Stripe.SetupIntent setupIntent)
|
||||
public static PaymentSource From(Stripe.SetupIntent setupIntent)
|
||||
{
|
||||
if (!setupIntent.IsUnverifiedBankAccount())
|
||||
{
|
||||
@ -36,13 +36,13 @@ public record MaskedPaymentMethodDTO(
|
||||
|
||||
var description = $"{bankAccount.BankName}, *{bankAccount.Last4}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
true);
|
||||
}
|
||||
|
||||
public static MaskedPaymentMethodDTO From(Braintree.Customer customer)
|
||||
public static PaymentSource From(Braintree.Customer customer)
|
||||
{
|
||||
var defaultPaymentMethod = customer.DefaultPaymentMethod;
|
||||
|
||||
@ -55,7 +55,7 @@ public record MaskedPaymentMethodDTO(
|
||||
{
|
||||
case Braintree.PayPalAccount payPalAccount:
|
||||
{
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.PayPal,
|
||||
payPalAccount.Email,
|
||||
false);
|
||||
@ -67,14 +67,14 @@ public record MaskedPaymentMethodDTO(
|
||||
var description =
|
||||
$"{creditCard.CardType}, *{creditCard.LastFour}, {paddedExpirationMonth}/{creditCard.ExpirationYear}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.Card,
|
||||
description,
|
||||
false);
|
||||
}
|
||||
case Braintree.UsBankAccount bankAccount:
|
||||
{
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.BankAccount,
|
||||
$"{bankAccount.BankName}, *{bankAccount.Last4}",
|
||||
false);
|
||||
@ -86,18 +86,18 @@ public record MaskedPaymentMethodDTO(
|
||||
}
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeBankAccountPaymentMethod(
|
||||
private static PaymentSource FromStripeBankAccountPaymentMethod(
|
||||
Stripe.PaymentMethodUsBankAccount bankAccount)
|
||||
{
|
||||
var description = $"{bankAccount.BankName}, *{bankAccount.Last4}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
false);
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeCardPaymentMethod(Stripe.PaymentMethodCard card)
|
||||
private static PaymentSource FromStripeCardPaymentMethod(Stripe.PaymentMethodCard card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
||||
@ -105,7 +105,7 @@ public record MaskedPaymentMethodDTO(
|
||||
|
||||
#region Legacy Source Payments
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeLegacyPaymentSource(Stripe.IPaymentSource paymentSource)
|
||||
private static PaymentSource FromStripeLegacyPaymentSource(Stripe.IPaymentSource paymentSource)
|
||||
=> paymentSource switch
|
||||
{
|
||||
Stripe.BankAccount bankAccount => FromStripeBankAccountLegacySource(bankAccount),
|
||||
@ -114,7 +114,7 @@ public record MaskedPaymentMethodDTO(
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeBankAccountLegacySource(Stripe.BankAccount bankAccount)
|
||||
private static PaymentSource FromStripeBankAccountLegacySource(Stripe.BankAccount bankAccount)
|
||||
{
|
||||
var status = bankAccount.Status switch
|
||||
{
|
||||
@ -128,19 +128,19 @@ public record MaskedPaymentMethodDTO(
|
||||
|
||||
var needsVerification = bankAccount.Status is "new" or "validated";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
return new PaymentSource(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
needsVerification);
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeCardLegacySource(Stripe.Card card)
|
||||
private static PaymentSource FromStripeCardLegacySource(Stripe.Card card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
||||
false);
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeSourceCardLegacySource(Stripe.SourceCard card)
|
||||
private static PaymentSource FromStripeSourceCardLegacySource(Stripe.SourceCard card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
@ -2,6 +2,6 @@
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record TokenizedPaymentMethodDTO(
|
||||
public record TokenizedPaymentSource(
|
||||
PaymentMethodType Type,
|
||||
string Token);
|
@ -4,5 +4,5 @@ namespace Bit.Core.Billing.Services;
|
||||
|
||||
public interface IOrganizationBillingService
|
||||
{
|
||||
Task<OrganizationMetadataDTO> GetMetadata(Guid organizationId);
|
||||
Task<OrganizationMetadata> GetMetadata(Guid organizationId);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
@ -47,21 +48,21 @@ public interface ISubscriberService
|
||||
CustomerGetOptions customerGetOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the account credit, a masked representation of the default payment method and the tax information for the
|
||||
/// provided <paramref name="subscriber"/>. This is essentially a consolidated invocation of the <see cref="GetPaymentMethod"/>
|
||||
/// Retrieves the account credit, a masked representation of the default payment source and the tax information for the
|
||||
/// provided <paramref name="subscriber"/>. This is essentially a consolidated invocation of the <see cref="GetPaymentSource"/>
|
||||
/// and <see cref="GetTaxInformation"/> methods with a response that includes the customer's <see cref="Stripe.Customer.Balance"/> as account credit in order to cut down on Stripe API calls.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to retrieve payment information for.</param>
|
||||
/// <returns>A <see cref="PaymentInformationDTO"/> containing the subscriber's account credit, masked payment method and tax information.</returns>
|
||||
Task<PaymentInformationDTO> GetPaymentInformation(
|
||||
/// <param name="subscriber">The subscriber to retrieve payment method for.</param>
|
||||
/// <returns>A <see cref="Models.PaymentMethod"/> containing the subscriber's account credit, payment source and tax information.</returns>
|
||||
Task<PaymentMethod> GetPaymentMethod(
|
||||
ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a masked representation of the subscriber's payment method for presentation to a client.
|
||||
/// Retrieves a masked representation of the subscriber's payment source for presentation to a client.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to retrieve the masked payment method for.</param>
|
||||
/// <returns>A <see cref="MaskedPaymentMethodDTO"/> containing a non-identifiable description of the subscriber's payment method.</returns>
|
||||
Task<MaskedPaymentMethodDTO> GetPaymentMethod(
|
||||
/// <param name="subscriber">The subscriber to retrieve the payment source for.</param>
|
||||
/// <returns>A <see cref="PaymentSource"/> containing a non-identifiable description of the subscriber's payment source. Example: VISA, *4242, 10/2026</returns>
|
||||
Task<PaymentSource> GetPaymentSource(
|
||||
ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
@ -100,25 +101,25 @@ public interface ISubscriberService
|
||||
ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove a subscriber's saved payment method. If the Stripe <see cref="Stripe.Customer"/> representing the
|
||||
/// Attempts to remove a subscriber's saved payment source. If the Stripe <see cref="Stripe.Customer"/> representing the
|
||||
/// <paramref name="subscriber"/> contains a valid <b>"btCustomerId"</b> key in its <see cref="Stripe.Customer.Metadata"/> property,
|
||||
/// this command will attempt to remove the Braintree <see cref="Braintree.PaymentMethod"/>. Otherwise, it will attempt to remove the
|
||||
/// Stripe <see cref="Stripe.PaymentMethod"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to remove the saved payment method for.</param>
|
||||
Task RemovePaymentMethod(ISubscriber subscriber);
|
||||
/// <param name="subscriber">The subscriber to remove the saved payment source for.</param>
|
||||
Task RemovePaymentSource(ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the payment method for the provided <paramref name="subscriber"/> using the <paramref name="tokenizedPaymentMethod"/>.
|
||||
/// The following payment method types are supported: [<see cref="PaymentMethodType.Card"/>, <see cref="PaymentMethodType.BankAccount"/>, <see cref="PaymentMethodType.PayPal"/>].
|
||||
/// For each type, updating the payment method will attempt to establish a new payment method using the token in the <see cref="TokenizedPaymentMethodDTO"/>. Then, it will
|
||||
/// remove the exising payment method(s) linked to the subscriber's customer.
|
||||
/// Updates the payment source for the provided <paramref name="subscriber"/> using the <paramref name="tokenizedPaymentSource"/>.
|
||||
/// The following types are supported: [<see cref="PaymentMethodType.Card"/>, <see cref="PaymentMethodType.BankAccount"/>, <see cref="PaymentMethodType.PayPal"/>].
|
||||
/// For each type, updating the payment source will attempt to establish a new payment source using the token in the <see cref="TokenizedPaymentSource"/>. Then, it will
|
||||
/// remove the exising payment source(s) linked to the subscriber's customer.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to update the payment method for.</param>
|
||||
/// <param name="tokenizedPaymentMethod">A DTO representing a tokenized payment method.</param>
|
||||
Task UpdatePaymentMethod(
|
||||
/// <param name="tokenizedPaymentSource">A DTO representing a tokenized payment method.</param>
|
||||
Task UpdatePaymentSource(
|
||||
ISubscriber subscriber,
|
||||
TokenizedPaymentMethodDTO tokenizedPaymentMethod);
|
||||
TokenizedPaymentSource tokenizedPaymentSource);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the tax information for the provided <paramref name="subscriber"/>.
|
||||
|
@ -11,7 +11,7 @@ public class OrganizationBillingService(
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||
{
|
||||
public async Task<OrganizationMetadataDTO> GetMetadata(Guid organizationId)
|
||||
public async Task<OrganizationMetadata> GetMetadata(Guid organizationId)
|
||||
{
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
@ -29,12 +29,12 @@ public class OrganizationBillingService(
|
||||
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return OrganizationMetadataDTO.Default();
|
||||
return OrganizationMetadata.Default();
|
||||
}
|
||||
|
||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
return new OrganizationMetadataDTO(isOnSecretsManagerStandalone);
|
||||
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
||||
}
|
||||
|
||||
private static bool IsOnSecretsManagerStandalone(
|
||||
|
@ -11,6 +11,7 @@ using Stripe;
|
||||
|
||||
using static Bit.Core.Billing.Utilities;
|
||||
using Customer = Stripe.Customer;
|
||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations;
|
||||
@ -175,34 +176,34 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PaymentInformationDTO> GetPaymentInformation(
|
||||
public async Task<PaymentMethod> GetPaymentMethod(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
|
||||
var customer = await GetCustomer(subscriber, new CustomerGetOptions
|
||||
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions
|
||||
{
|
||||
Expand = ["default_source", "invoice_settings.default_payment_method", "tax_ids"]
|
||||
Expand = ["default_source", "invoice_settings.default_payment_method", "subscriptions", "tax_ids"]
|
||||
});
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var accountCredit = customer.Balance * -1 / 100;
|
||||
|
||||
var paymentMethod = await GetMaskedPaymentMethodDTOAsync(subscriber.Id, customer);
|
||||
var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer);
|
||||
|
||||
var taxInformation = GetTaxInformationDTOFrom(customer);
|
||||
var subscriptionStatus = customer.Subscriptions
|
||||
.FirstOrDefault(subscription => subscription.Id == subscriber.GatewaySubscriptionId)?
|
||||
.Status;
|
||||
|
||||
return new PaymentInformationDTO(
|
||||
var taxInformation = GetTaxInformation(customer);
|
||||
|
||||
return new PaymentMethod(
|
||||
accountCredit,
|
||||
paymentMethod,
|
||||
subscriptionStatus,
|
||||
taxInformation);
|
||||
}
|
||||
|
||||
public async Task<MaskedPaymentMethodDTO> GetPaymentMethod(
|
||||
public async Task<PaymentSource> GetPaymentSource(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
@ -212,7 +213,7 @@ public class SubscriberService(
|
||||
Expand = ["default_source", "invoice_settings.default_payment_method"]
|
||||
});
|
||||
|
||||
return await GetMaskedPaymentMethodDTOAsync(subscriber.Id, customer);
|
||||
return await GetPaymentSourceAsync(subscriber.Id, customer);
|
||||
}
|
||||
|
||||
public async Task<Subscription> GetSubscription(
|
||||
@ -296,10 +297,10 @@ public class SubscriberService(
|
||||
|
||||
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||
|
||||
return GetTaxInformationDTOFrom(customer);
|
||||
return GetTaxInformation(customer);
|
||||
}
|
||||
|
||||
public async Task RemovePaymentMethod(
|
||||
public async Task RemovePaymentSource(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
@ -391,16 +392,16 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
public async Task UpdatePaymentSource(
|
||||
ISubscriber subscriber,
|
||||
TokenizedPaymentMethodDTO tokenizedPaymentMethod)
|
||||
TokenizedPaymentSource tokenizedPaymentSource)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
ArgumentNullException.ThrowIfNull(tokenizedPaymentMethod);
|
||||
ArgumentNullException.ThrowIfNull(tokenizedPaymentSource);
|
||||
|
||||
var customer = await GetCustomerOrThrow(subscriber);
|
||||
|
||||
var (type, token) = tokenizedPaymentMethod;
|
||||
var (type, token) = tokenizedPaymentSource;
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
@ -678,7 +679,7 @@ public class SubscriberService(
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
private async Task<MaskedPaymentMethodDTO> GetMaskedPaymentMethodDTOAsync(
|
||||
private async Task<PaymentSource> GetPaymentSourceAsync(
|
||||
Guid subscriberId,
|
||||
Customer customer)
|
||||
{
|
||||
@ -690,11 +691,11 @@ public class SubscriberService(
|
||||
{
|
||||
var braintreeCustomer = await braintreeGateway.Customer.FindAsync(braintreeCustomerId);
|
||||
|
||||
return MaskedPaymentMethodDTO.From(braintreeCustomer);
|
||||
return PaymentSource.From(braintreeCustomer);
|
||||
}
|
||||
}
|
||||
|
||||
var attachedPaymentMethodDTO = MaskedPaymentMethodDTO.From(customer);
|
||||
var attachedPaymentMethodDTO = PaymentSource.From(customer);
|
||||
|
||||
if (attachedPaymentMethodDTO != null)
|
||||
{
|
||||
@ -717,10 +718,10 @@ public class SubscriberService(
|
||||
Expand = ["payment_method"]
|
||||
});
|
||||
|
||||
return MaskedPaymentMethodDTO.From(setupIntent);
|
||||
return PaymentSource.From(setupIntent);
|
||||
}
|
||||
|
||||
private static TaxInformation GetTaxInformationDTOFrom(
|
||||
private static TaxInformation GetTaxInformation(
|
||||
Customer customer)
|
||||
{
|
||||
if (customer.Address == null)
|
||||
|
@ -139,6 +139,7 @@ public static class FeatureFlagKeys
|
||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
|
||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||
public const string AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
using PaymentMethod = Stripe.PaymentMethod;
|
||||
using StaticStore = Bit.Core.Models.StaticStore;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
|
||||
|
@ -12,6 +12,8 @@ using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
using static Bit.Api.Test.Billing.Utilities;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(OrganizationBillingController))]
|
||||
@ -27,7 +29,7 @@ public class OrganizationBillingControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
Assert.IsType<UnauthorizedHttpResult>(result);
|
||||
AssertUnauthorized(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -36,11 +38,11 @@ public class OrganizationBillingControllerTests
|
||||
SutProvider<OrganizationBillingController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId).Returns((OrganizationMetadataDTO)null);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId).Returns((OrganizationMetadata)null);
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
AssertNotFound(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -50,7 +52,7 @@ public class OrganizationBillingControllerTests
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadataDTO(true));
|
||||
.Returns(new OrganizationMetadata(true));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
@ -70,7 +72,7 @@ public class OrganizationBillingControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.GetHistoryAsync(organizationId);
|
||||
|
||||
Assert.IsType<UnauthorizedHttpResult>(result);
|
||||
AssertUnauthorized(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -83,7 +85,7 @@ public class OrganizationBillingControllerTests
|
||||
|
||||
var result = await sutProvider.Sut.GetHistoryAsync(organizationId);
|
||||
|
||||
Assert.IsType<NotFound>(result);
|
||||
AssertNotFound(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -107,7 +107,7 @@ public class ProviderBillingControllerTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
var invoices = new List<Invoice>
|
||||
{
|
||||
@ -187,7 +187,7 @@ public class ProviderBillingControllerTests
|
||||
string invoiceId,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IProviderBillingService>().GenerateClientInvoiceReport(invoiceId)
|
||||
.ReturnsNull();
|
||||
@ -208,7 +208,7 @@ public class ProviderBillingControllerTests
|
||||
string invoiceId,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
var reportContent = "Report"u8.ToArray();
|
||||
|
||||
@ -301,7 +301,7 @@ public class ProviderBillingControllerTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableServiceUserInputs(provider, sutProvider);
|
||||
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
|
||||
@ -432,7 +432,7 @@ public class ProviderBillingControllerTests
|
||||
TaxInformationRequestBody requestBody,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
requestBody.Country = null;
|
||||
|
||||
@ -451,7 +451,7 @@ public class ProviderBillingControllerTests
|
||||
TaxInformationRequestBody requestBody,
|
||||
SutProvider<ProviderBillingController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
||||
|
||||
|
@ -33,7 +33,7 @@ public class ProviderClientsControllerTests
|
||||
CreateClientOrganizationRequestBody requestBody,
|
||||
SutProvider<ProviderClientsController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
|
||||
|
||||
@ -48,7 +48,7 @@ public class ProviderClientsControllerTests
|
||||
CreateClientOrganizationRequestBody requestBody,
|
||||
SutProvider<ProviderClientsController> sutProvider)
|
||||
{
|
||||
ConfigureStableAdminInputs(provider, sutProvider);
|
||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
||||
|
||||
var user = new User();
|
||||
|
||||
@ -99,7 +99,7 @@ public class ProviderClientsControllerTests
|
||||
UpdateClientOrganizationRequestBody requestBody,
|
||||
SutProvider<ProviderClientsController> sutProvider)
|
||||
{
|
||||
ConfigureStableServiceUserInputs(provider, sutProvider);
|
||||
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
|
||||
.ReturnsNull();
|
||||
@ -118,7 +118,7 @@ public class ProviderClientsControllerTests
|
||||
Organization organization,
|
||||
SutProvider<ProviderClientsController> sutProvider)
|
||||
{
|
||||
ConfigureStableServiceUserInputs(provider, sutProvider);
|
||||
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
|
||||
.Returns(providerOrganization);
|
||||
@ -149,7 +149,7 @@ public class ProviderClientsControllerTests
|
||||
Organization organization,
|
||||
SutProvider<ProviderClientsController> sutProvider)
|
||||
{
|
||||
ConfigureStableServiceUserInputs(provider, sutProvider);
|
||||
ConfigureStableProviderServiceUserInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<IProviderOrganizationRepository>().GetByIdAsync(providerOrganizationId)
|
||||
.Returns(providerOrganization);
|
||||
|
@ -35,27 +35,27 @@ public static class Utilities
|
||||
Assert.Equal("Unauthorized.", response.Value.Message);
|
||||
}
|
||||
|
||||
public static void ConfigureStableAdminInputs<T>(
|
||||
public static void ConfigureStableProviderAdminInputs<T>(
|
||||
Provider provider,
|
||||
SutProvider<T> sutProvider) where T : BaseProviderController
|
||||
{
|
||||
ConfigureBaseInputs(provider, sutProvider);
|
||||
ConfigureBaseProviderInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
public static void ConfigureStableServiceUserInputs<T>(
|
||||
public static void ConfigureStableProviderServiceUserInputs<T>(
|
||||
Provider provider,
|
||||
SutProvider<T> sutProvider) where T : BaseProviderController
|
||||
{
|
||||
ConfigureBaseInputs(provider, sutProvider);
|
||||
ConfigureBaseProviderInputs(provider, sutProvider);
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderUser(provider.Id)
|
||||
.Returns(true);
|
||||
}
|
||||
|
||||
private static void ConfigureBaseInputs<T>(
|
||||
private static void ConfigureBaseProviderInputs<T>(
|
||||
Provider provider,
|
||||
SutProvider<T> sutProvider) where T : BaseProviderController
|
||||
{
|
||||
|
@ -330,7 +330,7 @@ public class SubscriberServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentMethod(null));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetPaymentMethod_Braintree_NoDefaultPaymentMethod_ReturnsNull(
|
||||
@ -364,7 +364,7 @@ public class SubscriberServiceTests
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Null(paymentMethod);
|
||||
}
|
||||
@ -407,7 +407,7 @@ public class SubscriberServiceTests
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.PayPal, paymentMethod.Type);
|
||||
Assert.Equal("a@example.com", paymentMethod.Description);
|
||||
@ -445,7 +445,7 @@ public class SubscriberServiceTests
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
|
||||
Assert.Equal("Chase, *9999", paymentMethod.Description);
|
||||
@ -481,7 +481,7 @@ public class SubscriberServiceTests
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
|
||||
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
|
||||
@ -527,7 +527,7 @@ public class SubscriberServiceTests
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id, Arg.Is<SetupIntentGetOptions>(
|
||||
options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
|
||||
Assert.Equal("Chase, *9999", paymentMethod.Description);
|
||||
@ -555,7 +555,7 @@ public class SubscriberServiceTests
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.BankAccount, paymentMethod.Type);
|
||||
Assert.Equal("Chase, *9999 - Verified", paymentMethod.Description);
|
||||
@ -584,7 +584,7 @@ public class SubscriberServiceTests
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
|
||||
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
|
||||
@ -616,7 +616,7 @@ public class SubscriberServiceTests
|
||||
options.Expand.Contains("invoice_settings.default_payment_method")))
|
||||
.Returns(customer);
|
||||
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentMethod(provider);
|
||||
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
|
||||
|
||||
Assert.Equal(PaymentMethodType.Card, paymentMethod.Type);
|
||||
Assert.Equal("VISA, *9999, 09/2028", paymentMethod.Description);
|
||||
@ -815,7 +815,7 @@ public class SubscriberServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentMethod(null));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.RemovePaymentSource(null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RemovePaymentMethod_Braintree_NoCustomer_ThrowsBillingException(
|
||||
@ -842,7 +842,7 @@ public class SubscriberServiceTests
|
||||
|
||||
braintreeGateway.Customer.Returns(customerGateway);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
|
||||
|
||||
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
|
||||
|
||||
@ -879,7 +879,7 @@ public class SubscriberServiceTests
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).Returns(braintreeCustomer);
|
||||
|
||||
await sutProvider.Sut.RemovePaymentMethod(organization);
|
||||
await sutProvider.Sut.RemovePaymentSource(organization);
|
||||
|
||||
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
|
||||
|
||||
@ -930,7 +930,7 @@ public class SubscriberServiceTests
|
||||
Arg.Is<CustomerRequest>(request => request.DefaultPaymentMethodToken == null))
|
||||
.Returns(updateBraintreeCustomerResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
|
||||
|
||||
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
|
||||
|
||||
@ -988,7 +988,7 @@ public class SubscriberServiceTests
|
||||
|
||||
paymentMethodGateway.DeleteAsync(paymentMethod.Token).Returns(deleteBraintreePaymentMethodResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentMethod(organization));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.RemovePaymentSource(organization));
|
||||
|
||||
await customerGateway.Received(1).FindAsync(braintreeCustomerId);
|
||||
|
||||
@ -1026,7 +1026,7 @@ public class SubscriberServiceTests
|
||||
.PaymentMethodListAutoPagingAsync(Arg.Any<PaymentMethodListOptions>())
|
||||
.Returns(GetPaymentMethodsAsync(new List<Stripe.PaymentMethod>()));
|
||||
|
||||
await sutProvider.Sut.RemovePaymentMethod(organization);
|
||||
await sutProvider.Sut.RemovePaymentSource(organization);
|
||||
|
||||
await stripeAdapter.Received(1).BankAccountDeleteAsync(stripeCustomer.Id, bankAccountId);
|
||||
|
||||
@ -1068,7 +1068,7 @@ public class SubscriberServiceTests
|
||||
}
|
||||
}));
|
||||
|
||||
await sutProvider.Sut.RemovePaymentMethod(organization);
|
||||
await sutProvider.Sut.RemovePaymentSource(organization);
|
||||
|
||||
await stripeAdapter.DidNotReceiveWithAnyArgs().BankAccountDeleteAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
|
||||
@ -1110,13 +1110,13 @@ public class SubscriberServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentMethod(null, null));
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(null, null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NullTokenizedPaymentMethod_ThrowsArgumentNullException(
|
||||
Provider provider,
|
||||
SutProvider<SubscriberService> sutProvider)
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentMethod(provider, null));
|
||||
=> await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdatePaymentSource(provider, null));
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdatePaymentMethod_NoToken_ThrowsBillingException(
|
||||
@ -1127,7 +1127,7 @@ public class SubscriberServiceTests
|
||||
.Returns(new Customer());
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.Card, null)));
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.Card, null)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1139,7 +1139,7 @@ public class SubscriberServiceTests
|
||||
.Returns(new Customer());
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.BitPay, "TOKEN")));
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BitPay, "TOKEN")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1156,7 +1156,7 @@ public class SubscriberServiceTests
|
||||
.Returns([new SetupIntent(), new SetupIntent()]);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.BankAccount, "TOKEN")));
|
||||
sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN")));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@ -1191,8 +1191,8 @@ public class SubscriberServiceTests
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentMethod(provider,
|
||||
new TokenizedPaymentMethodDTO(PaymentMethodType.BankAccount, "TOKEN"));
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.BankAccount, "TOKEN"));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_1");
|
||||
|
||||
@ -1232,8 +1232,8 @@ public class SubscriberServiceTests
|
||||
new PaymentMethod { Id = "payment_method_1" }
|
||||
]);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentMethod(provider,
|
||||
new TokenizedPaymentMethodDTO(PaymentMethodType.Card, "TOKEN"));
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.Card, "TOKEN"));
|
||||
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_2",
|
||||
Arg.Is<SetupIntentCancelOptions>(options => options.CancellationReason == "abandoned"));
|
||||
@ -1270,7 +1270,7 @@ public class SubscriberServiceTests
|
||||
|
||||
customerGateway.FindAsync(braintreeCustomerId).ReturnsNull();
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN")));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await paymentMethodGateway.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any<PaymentMethodRequest>());
|
||||
}
|
||||
@ -1308,7 +1308,7 @@ public class SubscriberServiceTests
|
||||
options => options.CustomerId == braintreeCustomerId && options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createPaymentMethodResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN")));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await customerGateway.DidNotReceiveWithAnyArgs().UpdateAsync(Arg.Any<string>(), Arg.Any<CustomerRequest>());
|
||||
}
|
||||
@ -1360,7 +1360,7 @@ public class SubscriberServiceTests
|
||||
options.DefaultPaymentMethodToken == createPaymentMethodResult.Target.Token))
|
||||
.Returns(updateCustomerResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentMethod(provider, new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN")));
|
||||
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.UpdatePaymentSource(provider, new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await paymentMethodGateway.Received(1).DeleteAsync(createPaymentMethodResult.Target.Token);
|
||||
}
|
||||
@ -1426,8 +1426,8 @@ public class SubscriberServiceTests
|
||||
|
||||
paymentMethodGateway.DeleteAsync(existingPaymentMethod.Token).Returns(deletePaymentMethodResult);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentMethod(provider,
|
||||
new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN"));
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
|
||||
|
||||
await paymentMethodGateway.Received(1).DeleteAsync(existingPaymentMethod.Token);
|
||||
}
|
||||
@ -1467,8 +1467,8 @@ public class SubscriberServiceTests
|
||||
.Returns(createCustomerResult);
|
||||
|
||||
await ThrowsBillingExceptionAsync(() =>
|
||||
sutProvider.Sut.UpdatePaymentMethod(provider,
|
||||
new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN")));
|
||||
sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN")));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().DidNotReceiveWithAnyArgs()
|
||||
.CustomerUpdateAsync(Arg.Any<string>(), Arg.Any<CustomerUpdateOptions>());
|
||||
@ -1513,8 +1513,8 @@ public class SubscriberServiceTests
|
||||
options.PaymentMethodNonce == "TOKEN"))
|
||||
.Returns(createCustomerResult);
|
||||
|
||||
await sutProvider.Sut.UpdatePaymentMethod(provider,
|
||||
new TokenizedPaymentMethodDTO(PaymentMethodType.PayPal, "TOKEN"));
|
||||
await sutProvider.Sut.UpdatePaymentSource(provider,
|
||||
new TokenizedPaymentSource(PaymentMethodType.PayPal, "TOKEN"));
|
||||
|
||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(provider.GatewayCustomerId,
|
||||
Arg.Is<CustomerUpdateOptions>(
|
||||
|
Loading…
Reference in New Issue
Block a user