diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 6cdb0e680..866d18f9a 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -155,7 +155,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv DaysUntilDue = 30 }); - await _subscriberService.RemovePaymentMethod(organization); + await _subscriberService.RemovePaymentSource(organization); } await _mailService.SendProviderUpdatePaymentMethod( diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 5a1d5cf38..064dae26d 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -173,7 +173,7 @@ public class RemoveOrganizationFromProviderCommandTests options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && options.DaysUntilDue == 30)); - await sutProvider.GetDependency().Received(1).RemovePaymentMethod(organization); + await sutProvider.GetDependency().Received(1).RemovePaymentSource(organization); await organizationRepository.Received(1).ReplaceAsync(Arg.Is(org => org.BillingEmail == "a@example.com")); diff --git a/src/Api/Billing/Controllers/BaseBillingController.cs b/src/Api/Billing/Controllers/BaseBillingController.cs new file mode 100644 index 000000000..81b8b29f2 --- /dev/null +++ b/src/Api/Billing/Controllers/BaseBillingController.cs @@ -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 BadRequest(Dictionary> errors) => + TypedResults.BadRequest(new ErrorResponseModel(errors)); + + public static BadRequest BadRequest(string message) => + TypedResults.BadRequest(new ErrorResponseModel(message)); + + public static NotFound NotFound() => + TypedResults.NotFound(new ErrorResponseModel("Resource not found.")); + + public static JsonHttpResult ServerError(string message = "Something went wrong with your request. Please contact support.") => + TypedResults.Json( + new ErrorResponseModel(message), + statusCode: StatusCodes.Status500InternalServerError); + + public static JsonHttpResult Unauthorized() => + TypedResults.Json( + new ErrorResponseModel("Unauthorized."), + statusCode: StatusCodes.Status401Unauthorized); + } +} diff --git a/src/Api/Billing/Controllers/BaseProviderController.cs b/src/Api/Billing/Controllers/BaseProviderController.cs index 37d804498..ecc9f23a7 100644 --- a/src/Api/Billing/Controllers/BaseProviderController.cs +++ b/src/Api/Billing/Controllers/BaseProviderController.cs @@ -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 logger, IProviderRepository providerRepository, - IUserService userService) : Controller + IUserService userService) : BaseBillingController { protected readonly IUserService UserService = userService; - protected static NotFound NotFoundResponse() => - TypedResults.NotFound(new ErrorResponseModel("Resource not found.")); - - protected static JsonHttpResult ServerErrorResponse(string errorMessage) => - TypedResults.Json( - new ErrorResponseModel(errorMessage), - statusCode: StatusCodes.Status500InternalServerError); - - protected static JsonHttpResult 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()); } } diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 47c4ef68f..1a2406d3d 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -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 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 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 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 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 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 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(); + } } diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 40a1ebdf2..eed7ed060 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -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( diff --git a/src/Api/Billing/Controllers/ProviderClientsController.cs b/src/Api/Billing/Controllers/ProviderClientsController.cs index 3fec4570f..d69499976 100644 --- a/src/Api/Billing/Controllers/ProviderClientsController.cs +++ b/src/Api/Billing/Controllers/ProviderClientsController.cs @@ -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); diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index 86b4b79cb..c5c0fde00 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -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); } diff --git a/src/Api/Billing/Models/Requests/TokenizedPaymentMethodRequestBody.cs b/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs similarity index 76% rename from src/Api/Billing/Models/Requests/TokenizedPaymentMethodRequestBody.cs rename to src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs index edb4e6b36..4f087913b 100644 --- a/src/Api/Billing/Models/Requests/TokenizedPaymentMethodRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs @@ -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( @@ -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); } diff --git a/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs b/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs new file mode 100644 index 000000000..cdc9a0885 --- /dev/null +++ b/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs @@ -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; } +} diff --git a/src/Api/Billing/Models/Responses/MaskedPaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/MaskedPaymentMethodResponse.cs deleted file mode 100644 index 36cf6969e..000000000 --- a/src/Api/Billing/Models/Responses/MaskedPaymentMethodResponse.cs +++ /dev/null @@ -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); -} diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 2323280d4..624eab1fe 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -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); } diff --git a/src/Api/Billing/Models/Responses/PaymentInformationResponse.cs b/src/Api/Billing/Models/Responses/PaymentInformationResponse.cs deleted file mode 100644 index 4ccb5889d..000000000 --- a/src/Api/Billing/Models/Responses/PaymentInformationResponse.cs +++ /dev/null @@ -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); -} diff --git a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs new file mode 100644 index 000000000..b89c1e9db --- /dev/null +++ b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs @@ -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); +} diff --git a/src/Api/Billing/Models/Responses/PaymentSourceResponse.cs b/src/Api/Billing/Models/Responses/PaymentSourceResponse.cs new file mode 100644 index 000000000..2c9a63b1d --- /dev/null +++ b/src/Api/Billing/Models/Responses/PaymentSourceResponse.cs @@ -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); +} diff --git a/src/Core/Billing/Models/BillingInfo.cs b/src/Core/Billing/Models/BillingInfo.cs index 5301c4eed..9bdc04257 100644 --- a/src/Core/Billing/Models/BillingInfo.cs +++ b/src/Core/Billing/Models/BillingInfo.cs @@ -12,7 +12,7 @@ public class BillingInfo { public BillingSource() { } - public BillingSource(PaymentMethod method) + public BillingSource(Stripe.PaymentMethod method) { if (method.Card == null) { diff --git a/src/Core/Billing/Models/OrganizationMetadataDTO.cs b/src/Core/Billing/Models/OrganizationMetadata.cs similarity index 56% rename from src/Core/Billing/Models/OrganizationMetadataDTO.cs rename to src/Core/Billing/Models/OrganizationMetadata.cs index e8f6c3422..decc35ffd 100644 --- a/src/Core/Billing/Models/OrganizationMetadataDTO.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -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); } diff --git a/src/Core/Billing/Models/PaymentInformationDTO.cs b/src/Core/Billing/Models/PaymentMethod.cs similarity index 51% rename from src/Core/Billing/Models/PaymentInformationDTO.cs rename to src/Core/Billing/Models/PaymentMethod.cs index 897d6a950..eed338d96 100644 --- a/src/Core/Billing/Models/PaymentInformationDTO.cs +++ b/src/Core/Billing/Models/PaymentMethod.cs @@ -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); diff --git a/src/Core/Billing/Models/MaskedPaymentMethodDTO.cs b/src/Core/Billing/Models/PaymentSource.cs similarity index 78% rename from src/Core/Billing/Models/MaskedPaymentMethodDTO.cs rename to src/Core/Billing/Models/PaymentSource.cs index 4a234ecc7..44bbddc66 100644 --- a/src/Core/Billing/Models/MaskedPaymentMethodDTO.cs +++ b/src/Core/Billing/Models/PaymentSource.cs @@ -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), diff --git a/src/Core/Billing/Models/TokenizedPaymentMethodDTO.cs b/src/Core/Billing/Models/TokenizedPaymentSource.cs similarity index 72% rename from src/Core/Billing/Models/TokenizedPaymentMethodDTO.cs rename to src/Core/Billing/Models/TokenizedPaymentSource.cs index 58d615c63..ad273e98d 100644 --- a/src/Core/Billing/Models/TokenizedPaymentMethodDTO.cs +++ b/src/Core/Billing/Models/TokenizedPaymentSource.cs @@ -2,6 +2,6 @@ namespace Bit.Core.Billing.Models; -public record TokenizedPaymentMethodDTO( +public record TokenizedPaymentSource( PaymentMethodType Type, string Token); diff --git a/src/Core/Billing/Services/IOrganizationBillingService.cs b/src/Core/Billing/Services/IOrganizationBillingService.cs index e030cd487..a4b522e2f 100644 --- a/src/Core/Billing/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Services/IOrganizationBillingService.cs @@ -4,5 +4,5 @@ namespace Bit.Core.Billing.Services; public interface IOrganizationBillingService { - Task GetMetadata(Guid organizationId); + Task GetMetadata(Guid organizationId); } diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index a9df11cee..ac2ac0ae7 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -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); /// - /// Retrieves the account credit, a masked representation of the default payment method and the tax information for the - /// provided . This is essentially a consolidated invocation of the + /// Retrieves the account credit, a masked representation of the default payment source and the tax information for the + /// provided . This is essentially a consolidated invocation of the /// and methods with a response that includes the customer's as account credit in order to cut down on Stripe API calls. /// - /// The subscriber to retrieve payment information for. - /// A containing the subscriber's account credit, masked payment method and tax information. - Task GetPaymentInformation( + /// The subscriber to retrieve payment method for. + /// A containing the subscriber's account credit, payment source and tax information. + Task GetPaymentMethod( ISubscriber subscriber); /// - /// 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. /// - /// The subscriber to retrieve the masked payment method for. - /// A containing a non-identifiable description of the subscriber's payment method. - Task GetPaymentMethod( + /// The subscriber to retrieve the payment source for. + /// A containing a non-identifiable description of the subscriber's payment source. Example: VISA, *4242, 10/2026 + Task GetPaymentSource( ISubscriber subscriber); /// @@ -100,25 +101,25 @@ public interface ISubscriberService ISubscriber subscriber); /// - /// Attempts to remove a subscriber's saved payment method. If the Stripe representing the + /// Attempts to remove a subscriber's saved payment source. If the Stripe representing the /// contains a valid "btCustomerId" key in its property, /// this command will attempt to remove the Braintree . Otherwise, it will attempt to remove the /// Stripe . /// - /// The subscriber to remove the saved payment method for. - Task RemovePaymentMethod(ISubscriber subscriber); + /// The subscriber to remove the saved payment source for. + Task RemovePaymentSource(ISubscriber subscriber); /// - /// Updates the payment method for the provided using the . - /// The following payment method types are supported: [, , ]. - /// For each type, updating the payment method will attempt to establish a new payment method using the token in the . Then, it will - /// remove the exising payment method(s) linked to the subscriber's customer. + /// Updates the payment source for the provided using the . + /// The following types are supported: [, , ]. + /// For each type, updating the payment source will attempt to establish a new payment source using the token in the . Then, it will + /// remove the exising payment source(s) linked to the subscriber's customer. /// /// The subscriber to update the payment method for. - /// A DTO representing a tokenized payment method. - Task UpdatePaymentMethod( + /// A DTO representing a tokenized payment method. + Task UpdatePaymentSource( ISubscriber subscriber, - TokenizedPaymentMethodDTO tokenizedPaymentMethod); + TokenizedPaymentSource tokenizedPaymentSource); /// /// Updates the tax information for the provided . diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 3013a269e..f5e6e7809 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -11,7 +11,7 @@ public class OrganizationBillingService( IOrganizationRepository organizationRepository, ISubscriberService subscriberService) : IOrganizationBillingService { - public async Task GetMetadata(Guid organizationId) + public async Task 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( diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 850e6737f..b133845b3 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -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 GetPaymentInformation( + public async Task 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 GetPaymentMethod( + public async Task 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 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 GetMaskedPaymentMethodDTOAsync( + private async Task 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) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index e35ec2804..98a984325 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -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 GetAllKeys() { diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 2fa84ef35..dd5adbda2 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -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; diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 7b8b00462..70ca59940 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -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(result); + AssertUnauthorized(result); } [Theory, BitAutoData] @@ -36,11 +38,11 @@ public class OrganizationBillingControllerTests SutProvider sutProvider) { sutProvider.GetDependency().AccessMembersTab(organizationId).Returns(true); - sutProvider.GetDependency().GetMetadata(organizationId).Returns((OrganizationMetadataDTO)null); + sutProvider.GetDependency().GetMetadata(organizationId).Returns((OrganizationMetadata)null); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); - Assert.IsType(result); + AssertNotFound(result); } [Theory, BitAutoData] @@ -50,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().AccessMembersTab(organizationId).Returns(true); sutProvider.GetDependency().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(result); + AssertUnauthorized(result); } [Theory, BitAutoData] @@ -83,7 +85,7 @@ public class OrganizationBillingControllerTests var result = await sutProvider.Sut.GetHistoryAsync(organizationId); - Assert.IsType(result); + AssertNotFound(result); } [Theory] diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 03f486342..73e3850c8 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -107,7 +107,7 @@ public class ProviderBillingControllerTests Provider provider, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); var invoices = new List { @@ -187,7 +187,7 @@ public class ProviderBillingControllerTests string invoiceId, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); sutProvider.GetDependency().GenerateClientInvoiceReport(invoiceId) .ReturnsNull(); @@ -208,7 +208,7 @@ public class ProviderBillingControllerTests string invoiceId, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); var reportContent = "Report"u8.ToArray(); @@ -301,7 +301,7 @@ public class ProviderBillingControllerTests Provider provider, SutProvider sutProvider) { - ConfigureStableServiceUserInputs(provider, sutProvider); + ConfigureStableProviderServiceUserInputs(provider, sutProvider); var stripeAdapter = sutProvider.GetDependency(); @@ -432,7 +432,7 @@ public class ProviderBillingControllerTests TaxInformationRequestBody requestBody, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); requestBody.Country = null; @@ -451,7 +451,7 @@ public class ProviderBillingControllerTests TaxInformationRequestBody requestBody, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody); diff --git a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs index d0a79e15c..450ff9bf2 100644 --- a/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderClientsControllerTests.cs @@ -33,7 +33,7 @@ public class ProviderClientsControllerTests CreateClientOrganizationRequestBody requestBody, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()).ReturnsNull(); @@ -48,7 +48,7 @@ public class ProviderClientsControllerTests CreateClientOrganizationRequestBody requestBody, SutProvider sutProvider) { - ConfigureStableAdminInputs(provider, sutProvider); + ConfigureStableProviderAdminInputs(provider, sutProvider); var user = new User(); @@ -99,7 +99,7 @@ public class ProviderClientsControllerTests UpdateClientOrganizationRequestBody requestBody, SutProvider sutProvider) { - ConfigureStableServiceUserInputs(provider, sutProvider); + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) .ReturnsNull(); @@ -118,7 +118,7 @@ public class ProviderClientsControllerTests Organization organization, SutProvider sutProvider) { - ConfigureStableServiceUserInputs(provider, sutProvider); + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) .Returns(providerOrganization); @@ -149,7 +149,7 @@ public class ProviderClientsControllerTests Organization organization, SutProvider sutProvider) { - ConfigureStableServiceUserInputs(provider, sutProvider); + ConfigureStableProviderServiceUserInputs(provider, sutProvider); sutProvider.GetDependency().GetByIdAsync(providerOrganizationId) .Returns(providerOrganization); diff --git a/test/Api.Test/Billing/Utilities.cs b/test/Api.Test/Billing/Utilities.cs index ce528477d..36291ec71 100644 --- a/test/Api.Test/Billing/Utilities.cs +++ b/test/Api.Test/Billing/Utilities.cs @@ -35,27 +35,27 @@ public static class Utilities Assert.Equal("Unauthorized.", response.Value.Message); } - public static void ConfigureStableAdminInputs( + public static void ConfigureStableProviderAdminInputs( Provider provider, SutProvider sutProvider) where T : BaseProviderController { - ConfigureBaseInputs(provider, sutProvider); + ConfigureBaseProviderInputs(provider, sutProvider); sutProvider.GetDependency().ProviderProviderAdmin(provider.Id) .Returns(true); } - public static void ConfigureStableServiceUserInputs( + public static void ConfigureStableProviderServiceUserInputs( Provider provider, SutProvider sutProvider) where T : BaseProviderController { - ConfigureBaseInputs(provider, sutProvider); + ConfigureBaseProviderInputs(provider, sutProvider); sutProvider.GetDependency().ProviderUser(provider.Id) .Returns(true); } - private static void ConfigureBaseInputs( + private static void ConfigureBaseProviderInputs( Provider provider, SutProvider sutProvider) where T : BaseProviderController { diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 6cbb3fb67..652c22764 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -330,7 +330,7 @@ public class SubscriberServiceTests [Theory, BitAutoData] public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException( SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.GetPaymentMethod(null)); + await Assert.ThrowsAsync(() => 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().SetupIntentGet(setupIntent.Id, Arg.Is( 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 sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.RemovePaymentMethod(null)); + await Assert.ThrowsAsync(() => 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(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()) .Returns(GetPaymentMethodsAsync(new List())); - 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(), Arg.Any()); @@ -1110,13 +1110,13 @@ public class SubscriberServiceTests [Theory, BitAutoData] public async Task UpdatePaymentMethod_NullSubscriber_ThrowsArgumentNullException( SutProvider sutProvider) - => await Assert.ThrowsAsync(() => sutProvider.Sut.UpdatePaymentMethod(null, null)); + => await Assert.ThrowsAsync(() => sutProvider.Sut.UpdatePaymentSource(null, null)); [Theory, BitAutoData] public async Task UpdatePaymentMethod_NullTokenizedPaymentMethod_ThrowsArgumentNullException( Provider provider, SutProvider sutProvider) - => await Assert.ThrowsAsync(() => sutProvider.Sut.UpdatePaymentMethod(provider, null)); + => await Assert.ThrowsAsync(() => 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().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(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()); } @@ -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(), Arg.Any()); } @@ -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().DidNotReceiveWithAnyArgs() .CustomerUpdateAsync(Arg.Any(), Arg.Any()); @@ -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().Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is(