diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index 7abcf8c357..99b6a47da0 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,7 +1,5 @@ -#nullable enable -using Bit.Api.Billing.Models.Responses; +using Bit.Api.Billing.Models.Responses; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Requests; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -16,6 +14,7 @@ public class AccountsBillingController( IUserService userService, IPaymentHistoryService paymentHistoryService) : Controller { + // TODO: Migrate to Query / AccountBillingVNextController [HttpGet("history")] [SelfHosted(NotSelfHostedOnly = true)] public async Task GetBillingHistoryAsync() @@ -30,20 +29,7 @@ public class AccountsBillingController( return new BillingHistoryResponseModel(billingInfo); } - [HttpGet("payment-method")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task GetPaymentMethodAsync() - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var billingInfo = await paymentService.GetBillingAsync(user); - return new BillingPaymentResponseModel(billingInfo); - } - + // TODO: Migrate to Query / AccountBillingVNextController [HttpGet("invoices")] public async Task GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null) { @@ -62,6 +48,7 @@ public class AccountsBillingController( return TypedResults.Ok(invoices); } + // TODO: Migrate to Query / AccountBillingVNextController [HttpGet("transactions")] public async Task GetTransactionsAsync([FromQuery] DateTime? startAfter = null) { @@ -78,18 +65,4 @@ public class AccountsBillingController( return TypedResults.Ok(transactions); } - - [HttpPost("preview-invoice")] - public async Task PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId); - - return TypedResults.Ok(invoice); - } } diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 506ce13e4e..e136513c77 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -1,6 +1,4 @@ -#nullable enable - -using Bit.Api.Models.Request; +using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; @@ -29,6 +27,7 @@ public class AccountsController( IFeatureService featureService, ILicensingService licensingService) : Controller { + // TODO: Remove when pm-24996-implement-upgrade-from-free-dialog is removed [HttpPost("premium")] public async Task PostPremiumAsync( PremiumRequestModel model, @@ -76,6 +75,7 @@ public class AccountsController( }; } + // TODO: Migrate to Query / AccountBillingVNextController as part of Premium -> Organization upgrade work. [HttpGet("subscription")] public async Task GetSubscriptionAsync( [FromServices] GlobalSettings globalSettings, @@ -114,29 +114,7 @@ public class AccountsController( } } - [HttpPost("payment")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostPaymentAsync([FromBody] PaymentRequestModel model) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - await userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType!.Value, - new TaxInfo - { - BillingAddressLine1 = model.Line1, - BillingAddressLine2 = model.Line2, - BillingAddressCity = model.City, - BillingAddressState = model.State, - BillingAddressCountry = model.Country, - BillingAddressPostalCode = model.PostalCode, - TaxIdNumber = model.TaxId - }); - } - + // TODO: Migrate to Command / AccountBillingVNextController as PUT /account/billing/vnext/subscription [HttpPost("storage")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostStorageAsync([FromBody] StorageRequestModel model) @@ -151,8 +129,11 @@ public class AccountsController( return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result }; } - - + /* + * TODO: A new version of this exists in the AccountBillingVNextController. + * The individual-self-hosting-license-uploader.component needs to be updated to use it. + * Then, this can be removed. + */ [HttpPost("license")] [SelfHosted(SelfHostedOnly = true)] public async Task PostLicenseAsync(LicenseRequestModel model) @@ -172,6 +153,7 @@ public class AccountsController( await userService.UpdateLicenseAsync(user, license); } + // TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription [HttpPost("cancel")] public async Task PostCancelAsync( [FromBody] SubscriptionCancellationRequestModel request, @@ -189,6 +171,7 @@ public class AccountsController( user.IsExpired()); } + // TODO: Migrate to Command / AccountBillingVNextController as POST /account/billing/vnext/subscription/reinstate [HttpPost("reinstate-premium")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostReinstateAsync() @@ -202,41 +185,6 @@ public class AccountsController( await userService.ReinstatePremiumAsync(user); } - [HttpGet("tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task GetTaxInfoAsync( - [FromServices] IPaymentService paymentService) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var taxInfo = await paymentService.GetTaxInfoAsync(user); - return new TaxInfoResponseModel(taxInfo); - } - - [HttpPut("tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PutTaxInfoAsync( - [FromBody] TaxInfoUpdateRequestModel model, - [FromServices] IPaymentService paymentService) - { - var user = await userService.GetUserByPrincipalAsync(User); - if (user == null) - { - throw new UnauthorizedAccessException(); - } - - var taxInfo = new TaxInfo - { - BillingAddressPostalCode = model.PostalCode, - BillingAddressCountry = model.Country, - }; - await paymentService.SaveTaxInfoAsync(user, taxInfo); - } - private async Task> GetOrganizationIdsClaimingUserAsync(Guid userId) { var organizationsClaimingUser = await userService.GetOrganizationsClaimingUserAsync(userId); diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs deleted file mode 100644 index 30ea975e09..0000000000 --- a/src/Api/Billing/Controllers/InvoicesController.cs +++ /dev/null @@ -1,45 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Tax.Requests; -using Bit.Core.Context; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Billing.Controllers; - -[Route("invoices")] -[Authorize("Application")] -public class InvoicesController : BaseBillingController -{ - [HttpPost("preview-organization")] - public async Task PreviewInvoiceAsync( - [FromBody] PreviewOrganizationInvoiceRequestBody model, - [FromServices] ICurrentContext currentContext, - [FromServices] IOrganizationRepository organizationRepository, - [FromServices] IPaymentService paymentService) - { - Organization organization = null; - if (model.OrganizationId != default) - { - if (!await currentContext.EditPaymentMethods(model.OrganizationId)) - { - return Error.Unauthorized(); - } - - organization = await organizationRepository.GetByIdAsync(model.OrganizationId); - if (organization == null) - { - return Error.NotFound(); - } - } - - var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId, - organization?.GatewaySubscriptionId); - - return TypedResults.Ok(invoice); - } -} diff --git a/src/Api/Billing/Controllers/LicensesController.cs b/src/Api/Billing/Controllers/LicensesController.cs deleted file mode 100644 index 29313bd4d8..0000000000 --- a/src/Api/Billing/Controllers/LicensesController.cs +++ /dev/null @@ -1,91 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; -using Bit.Core.Billing.Models.Business; -using Bit.Core.Billing.Organizations.Models; -using Bit.Core.Billing.Organizations.Queries; -using Bit.Core.Context; -using Bit.Core.Exceptions; -using Bit.Core.Models.Api.OrganizationLicenses; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Utilities; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Billing.Controllers; - -[Route("licenses")] -[Authorize("Licensing")] -[SelfHosted(NotSelfHostedOnly = true)] -public class LicensesController : Controller -{ - private readonly IUserRepository _userRepository; - private readonly IUserService _userService; - private readonly IOrganizationRepository _organizationRepository; - private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery; - private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; - private readonly ICurrentContext _currentContext; - - public LicensesController( - IUserRepository userRepository, - IUserService userService, - IOrganizationRepository organizationRepository, - IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery, - IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, - ICurrentContext currentContext) - { - _userRepository = userRepository; - _userService = userService; - _organizationRepository = organizationRepository; - _getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery; - _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; - _currentContext = currentContext; - } - - [HttpGet("user/{id}")] - public async Task GetUser(string id, [FromQuery] string key) - { - var user = await _userRepository.GetByIdAsync(new Guid(id)); - if (user == null) - { - return null; - } - else if (!user.LicenseKey.Equals(key)) - { - await Task.Delay(2000); - throw new BadRequestException("Invalid license key."); - } - - var license = await _userService.GenerateLicenseAsync(user, null); - return license; - } - - /// - /// Used by self-hosted installations to get an updated license file - /// - [HttpGet("organization/{id}")] - public async Task OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model) - { - var organization = await _organizationRepository.GetByIdAsync(new Guid(id)); - if (organization == null) - { - throw new NotFoundException("Organization not found."); - } - - if (!organization.LicenseKey.Equals(model.LicenseKey)) - { - await Task.Delay(2000); - throw new BadRequestException("Invalid license key."); - } - - if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey)) - { - throw new BadRequestException("Invalid Billing Sync Key"); - } - - var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value); - return license; - } -} diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 6e4cacc155..a0a3e48b60 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -20,9 +20,9 @@ public class OrganizationBillingController( IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, IPaymentService paymentService, - ISubscriberService subscriberService, IPaymentHistoryService paymentHistoryService) : BaseBillingController { + // TODO: Remove when pm-25379-use-new-organization-metadata-structure is removed. [HttpGet("metadata")] public async Task GetMetadataAsync([FromRoute] Guid organizationId) { @@ -41,6 +41,7 @@ public class OrganizationBillingController( return TypedResults.Ok(metadata); } + // TODO: Migrate to Query / OrganizationBillingVNextController [HttpGet("history")] public async Task GetHistoryAsync([FromRoute] Guid organizationId) { @@ -61,6 +62,7 @@ public class OrganizationBillingController( return TypedResults.Ok(billingInfo); } + // TODO: Migrate to Query / OrganizationBillingVNextController [HttpGet("invoices")] public async Task GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null) { @@ -85,6 +87,7 @@ public class OrganizationBillingController( return TypedResults.Ok(invoices); } + // TODO: Migrate to Query / OrganizationBillingVNextController [HttpGet("transactions")] public async Task GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null) { @@ -108,6 +111,7 @@ public class OrganizationBillingController( return TypedResults.Ok(transactions); } + // TODO: Can be removed once we do away with the organization-plans.component. [HttpGet] [SelfHosted(NotSelfHostedOnly = true)] public async Task GetBillingAsync(Guid organizationId) @@ -131,127 +135,7 @@ public class OrganizationBillingController( return TypedResults.Ok(response); } - [HttpGet("payment-method")] - public async Task GetPaymentMethodAsync([FromRoute] Guid organizationId) - { - 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 (!await currentContext.EditPaymentMethods(organizationId)) - { - return Error.Unauthorized(); - } - - var organization = await organizationRepository.GetByIdAsync(organizationId); - - if (organization == null) - { - return Error.NotFound(); - } - - var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain(); - - var taxInformation = requestBody.TaxInformation.ToDomain(); - - await organizationBillingService.UpdatePaymentMethod(organization, tokenizedPaymentSource, taxInformation); - - return TypedResults.Ok(); - } - - [HttpPost("payment-method/verify-bank-account")] - public async Task VerifyBankAccountAsync( - [FromRoute] Guid organizationId, - [FromBody] VerifyBankAccountRequestBody requestBody) - { - if (!await currentContext.EditPaymentMethods(organizationId)) - { - return Error.Unauthorized(); - } - - if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM")) - { - return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'"); - } - - var organization = await organizationRepository.GetByIdAsync(organizationId); - - if (organization == null) - { - return Error.NotFound(); - } - - await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode); - - return TypedResults.Ok(); - } - - [HttpGet("tax-information")] - public async Task GetTaxInformationAsync([FromRoute] Guid organizationId) - { - 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 (!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(); - } - + // TODO: Migrate to Command / OrganizationBillingVNextController [HttpPost("setup-business-unit")] [SelfHosted(NotSelfHostedOnly = true)] public async Task SetupBusinessUnitAsync( @@ -280,6 +164,7 @@ public class OrganizationBillingController( return TypedResults.Ok(providerId); } + // TODO: Migrate to Command / OrganizationBillingVNextController [HttpPost("change-frequency")] [SelfHosted(NotSelfHostedOnly = true)] public async Task ChangePlanSubscriptionFrequencyAsync( diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 6b8061c03c..16fb00a3e7 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -19,7 +19,6 @@ using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -249,53 +248,6 @@ public class OrganizationsController( await organizationService.ReinstateSubscriptionAsync(id); } - [HttpGet("{id:guid}/tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task GetTaxInfo(Guid id) - { - if (!await currentContext.OrganizationOwner(id)) - { - throw new NotFoundException(); - } - - var organization = await organizationRepository.GetByIdAsync(id); - if (organization == null) - { - throw new NotFoundException(); - } - - var taxInfo = await paymentService.GetTaxInfoAsync(organization); - return new TaxInfoResponseModel(taxInfo); - } - - [HttpPut("{id:guid}/tax")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PutTaxInfo(Guid id, [FromBody] ExpandedTaxInfoUpdateRequestModel model) - { - if (!await currentContext.OrganizationOwner(id)) - { - throw new NotFoundException(); - } - - var organization = await organizationRepository.GetByIdAsync(id); - if (organization == null) - { - throw new NotFoundException(); - } - - var taxInfo = new TaxInfo - { - TaxIdNumber = model.TaxId, - BillingAddressLine1 = model.Line1, - BillingAddressLine2 = model.Line2, - BillingAddressCity = model.City, - BillingAddressState = model.State, - BillingAddressPostalCode = model.PostalCode, - BillingAddressCountry = model.Country, - }; - await paymentService.SaveTaxInfoAsync(organization, taxInfo); - } - /// /// Tries to grant owner access to the Secrets Manager for the organization /// diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 006a7ce068..d358f8efd2 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,7 +1,6 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Pricing; @@ -9,7 +8,6 @@ using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.BitStripe; using Bit.Core.Services; @@ -34,6 +32,7 @@ public class ProviderBillingController( IStripeAdapter stripeAdapter, IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService) { + // TODO: Migrate to Query / ProviderBillingVNextController [HttpGet("invoices")] public async Task GetInvoicesAsync([FromRoute] Guid providerId) { @@ -54,6 +53,7 @@ public class ProviderBillingController( return TypedResults.Ok(response); } + // TODO: Migrate to Query / ProviderBillingVNextController [HttpGet("invoices/{invoiceId}")] public async Task GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId) { @@ -76,51 +76,7 @@ public class ProviderBillingController( "text/csv"); } - [HttpPut("payment-method")] - public async Task UpdatePaymentMethodAsync( - [FromRoute] Guid providerId, - [FromBody] UpdatePaymentMethodRequestBody requestBody) - { - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); - - if (provider == null) - { - return result; - } - - var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain(); - var taxInformation = requestBody.TaxInformation.ToDomain(); - - await providerBillingService.UpdatePaymentMethod( - provider, - tokenizedPaymentSource, - taxInformation); - - return TypedResults.Ok(); - } - - [HttpPost("payment-method/verify-bank-account")] - public async Task VerifyBankAccountAsync( - [FromRoute] Guid providerId, - [FromBody] VerifyBankAccountRequestBody requestBody) - { - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); - - if (provider == null) - { - return result; - } - - if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM")) - { - return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'"); - } - - await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode); - - return TypedResults.Ok(); - } - + // TODO: Migrate to Query / ProviderBillingVNextController [HttpGet("subscription")] public async Task GetSubscriptionAsync([FromRoute] Guid providerId) { @@ -172,53 +128,4 @@ public class ProviderBillingController( return TypedResults.Ok(response); } - - [HttpGet("tax-information")] - public async Task GetTaxInformationAsync([FromRoute] Guid providerId) - { - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); - - if (provider == null) - { - return result; - } - - var taxInformation = await subscriberService.GetTaxInformation(provider); - - var response = TaxInformationResponse.From(taxInformation); - - return TypedResults.Ok(response); - } - - [HttpPut("tax-information")] - public async Task UpdateTaxInformationAsync( - [FromRoute] Guid providerId, - [FromBody] TaxInformationRequestBody requestBody) - { - var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId); - - if (provider == null) - { - return result; - } - - if (requestBody is not { Country: not null, PostalCode: not null }) - { - return Error.BadRequest("Country and postal code are required to update your tax information."); - } - - var taxInformation = new TaxInformation( - requestBody.Country, - requestBody.PostalCode, - requestBody.TaxId, - requestBody.TaxIdType, - requestBody.Line1, - requestBody.Line2, - requestBody.City, - requestBody.State); - - await subscriberService.UpdateTaxInformation(provider, taxInformation); - - return TypedResults.Ok(); - } } diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs similarity index 92% rename from src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs rename to src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs index 973a7d99a1..b86f29bdbc 100644 --- a/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingController.cs +++ b/src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs @@ -1,5 +1,4 @@ -#nullable enable -using Bit.Api.Billing.Attributes; +using Bit.Api.Billing.Attributes; using Bit.Api.Billing.Models.Requests.Premium; using Bit.Api.Utilities; using Bit.Core; @@ -17,7 +16,7 @@ namespace Bit.Api.Billing.Controllers.VNext; [Authorize("Application")] [Route("account/billing/vnext/self-host")] [SelfHosted(SelfHostedOnly = true)] -public class SelfHostedAccountBillingController( +public class SelfHostedAccountBillingVNextController( ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController { [HttpPost("license")] diff --git a/src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs b/src/Api/Billing/Controllers/VNext/SelfHostedOrganizationBillingVNextController.cs similarity index 95% rename from src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs rename to src/Api/Billing/Controllers/VNext/SelfHostedOrganizationBillingVNextController.cs index bd40c777dc..625a97c998 100644 --- a/src/Api/Billing/Controllers/VNext/SelfHostedBillingController.cs +++ b/src/Api/Billing/Controllers/VNext/SelfHostedOrganizationBillingVNextController.cs @@ -14,7 +14,7 @@ namespace Bit.Api.Billing.Controllers.VNext; [Authorize("Application")] [Route("organizations/{organizationId:guid}/billing/vnext/self-host")] [SelfHosted(SelfHostedOnly = true)] -public class SelfHostedBillingController( +public class SelfHostedOrganizationBillingVNextController( IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController { [Authorize] diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs deleted file mode 100644 index a1b754a9dc..0000000000 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ /dev/null @@ -1,31 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; -using Bit.Core.Billing.Tax.Models; - -namespace Bit.Api.Billing.Models.Requests; - -public class TaxInformationRequestBody -{ - [Required] - public string Country { get; set; } - [Required] - public string PostalCode { get; set; } - public string TaxId { get; set; } - public string TaxIdType { get; set; } - public string Line1 { get; set; } - public string Line2 { get; set; } - public string City { get; set; } - public string State { get; set; } - - public TaxInformation ToDomain() => new( - Country, - PostalCode, - TaxId, - TaxIdType, - Line1, - Line2, - City, - State); -} diff --git a/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs b/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs deleted file mode 100644 index b469ce2576..0000000000 --- a/src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs +++ /dev/null @@ -1,25 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -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 TokenizedPaymentSourceRequestBody -{ - [Required] - [EnumMatches( - PaymentMethodType.BankAccount, - PaymentMethodType.Card, - 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 deleted file mode 100644 index 05ab1e34c9..0000000000 --- a/src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs +++ /dev/null @@ -1,15 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -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/Requests/VerifyBankAccountRequestBody.cs b/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs deleted file mode 100644 index e248d55dde..0000000000 --- a/src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs +++ /dev/null @@ -1,12 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using System.ComponentModel.DataAnnotations; - -namespace Bit.Api.Billing.Models.Requests; - -public class VerifyBankAccountRequestBody -{ - [Required] - public string DescriptorCode { get; set; } -} diff --git a/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs b/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs deleted file mode 100644 index f305e41c4f..0000000000 --- a/src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs +++ /dev/null @@ -1,20 +0,0 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.Billing.Models; -using Bit.Core.Models.Api; - -namespace Bit.Api.Billing.Models.Responses; - -public class BillingPaymentResponseModel : ResponseModel -{ - public BillingPaymentResponseModel(BillingInfo billing) - : base("billingPayment") - { - Balance = billing.Balance; - PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; - } - - public decimal Balance { get; set; } - public BillingSource PaymentSource { get; set; } -} diff --git a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs deleted file mode 100644 index a54ac0a876..0000000000 --- a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bit.Core.Billing.Models; -using Bit.Core.Billing.Tax.Models; - -namespace Bit.Api.Billing.Models.Responses; - -public record PaymentMethodResponse( - decimal 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 deleted file mode 100644 index 2c9a63b1d0..0000000000 --- a/src/Api/Billing/Models/Responses/PaymentSourceResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/Api/Billing/Models/Responses/TaxInformationResponse.cs b/src/Api/Billing/Models/Responses/TaxInformationResponse.cs deleted file mode 100644 index 59e4934751..0000000000 --- a/src/Api/Billing/Models/Responses/TaxInformationResponse.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Bit.Core.Billing.Tax.Models; - -namespace Bit.Api.Billing.Models.Responses; - -public record TaxInformationResponse( - string Country, - string PostalCode, - string TaxId, - string Line1, - string Line2, - string City, - string State) -{ - public static TaxInformationResponse From(TaxInformation taxInformation) - => new( - taxInformation.Country, - taxInformation.PostalCode, - taxInformation.TaxId, - taxInformation.Line1, - taxInformation.Line2, - taxInformation.City, - taxInformation.State); -} diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index f88727f37b..343a0e4f38 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -6,7 +6,6 @@ using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Stripe; -using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod; namespace Bit.Core.Billing.Services; @@ -64,16 +63,6 @@ public interface ISubscriberService ISubscriber subscriber, CustomerGetOptions customerGetOptions = null); - /// - /// 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 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 source for presentation to a client. /// @@ -107,16 +96,6 @@ public interface ISubscriberService ISubscriber subscriber, SubscriptionGetOptions subscriptionGetOptions = null); - /// - /// Retrieves the 's tax information using their Stripe 's . - /// - /// The subscriber to retrieve the tax information for. - /// A representing the 's tax information. - /// Thrown when the is . - /// This method opts for returning rather than throwing exceptions, making it ideal for surfacing data from API endpoints. - Task GetTaxInformation( - ISubscriber subscriber); - /// /// Attempts to remove a subscriber's saved payment source. If the Stripe representing the /// contains a valid "btCustomerId" key in its property, @@ -147,17 +126,6 @@ public interface ISubscriberService ISubscriber subscriber, TaxInformation taxInformation); - /// - /// Verifies the subscriber's pending bank account using the provided . - /// - /// The subscriber to verify the bank account for. - /// The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it. - /// Learn more. - /// - Task VerifyBankAccount( - ISubscriber subscriber, - string descriptorCode); - /// /// Validates whether the 's exists in the gateway. /// If the 's is or empty, returns . diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 8e75bf3dca..4b2ea26294 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -24,7 +24,6 @@ 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; @@ -330,38 +329,6 @@ public class SubscriberService( } } - public async Task GetPaymentMethod( - ISubscriber subscriber) - { - ArgumentNullException.ThrowIfNull(subscriber); - - var customer = await GetCustomer(subscriber, new CustomerGetOptions - { - Expand = ["default_source", "invoice_settings.default_payment_method", "subscriptions", "tax_ids"] - }); - - if (customer == null) - { - return PaymentMethod.Empty; - } - - var accountCredit = customer.Balance * -1 / 100M; - - var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer); - - var subscriptionStatus = customer.Subscriptions - .FirstOrDefault(subscription => subscription.Id == subscriber.GatewaySubscriptionId)? - .Status; - - var taxInformation = GetTaxInformation(customer); - - return new PaymentMethod( - accountCredit, - paymentMethod, - subscriptionStatus, - taxInformation); - } - public async Task GetPaymentSource( ISubscriber subscriber) { @@ -449,16 +416,6 @@ public class SubscriberService( } } - public async Task GetTaxInformation( - ISubscriber subscriber) - { - ArgumentNullException.ThrowIfNull(subscriber); - - var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions { Expand = ["tax_ids"] }); - - return GetTaxInformation(customer); - } - public async Task RemovePaymentSource( ISubscriber subscriber) { @@ -823,57 +780,6 @@ public class SubscriberService( } } - public async Task VerifyBankAccount( - ISubscriber subscriber, - string descriptorCode) - { - var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id); - - if (string.IsNullOrEmpty(setupIntentId)) - { - logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id); - throw new BillingException(); - } - - try - { - await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId, - new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode }); - - var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId); - - await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId, - new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - - await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, - new CustomerUpdateOptions - { - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = setupIntent.PaymentMethodId - } - }); - } - catch (StripeException stripeException) - { - if (!string.IsNullOrEmpty(stripeException.StripeError?.Code)) - { - var message = stripeException.StripeError.Code switch - { - StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.", - StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.", - StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.", - _ => BillingException.DefaultMessage - }; - - throw new BadRequestException(message); - } - - logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id); - throw new BillingException(); - } - } - public async Task IsValidGatewayCustomerIdAsync(ISubscriber subscriber) { ArgumentNullException.ThrowIfNull(subscriber); @@ -970,25 +876,6 @@ public class SubscriberService( return PaymentSource.From(setupIntent); } - private static TaxInformation GetTaxInformation( - Customer customer) - { - if (customer.Address == null) - { - return null; - } - - return new TaxInformation( - customer.Address.Country, - customer.Address.PostalCode, - customer.TaxIds?.FirstOrDefault()?.Value, - customer.TaxIds?.FirstOrDefault()?.Type, - customer.Address.Line1, - customer.Address.Line2, - customer.Address.City, - customer.Address.State); - } - private async Task RemoveBraintreeCustomerIdAsync( Customer customer) { diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index e7e848bcba..b4a4639992 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -4,8 +4,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Tax.Requests; -using Bit.Core.Billing.Tax.Responses; using Bit.Core.Entities; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; @@ -44,8 +42,6 @@ public interface IPaymentService Task GetBillingAsync(ISubscriber subscriber); Task GetBillingHistoryAsync(ISubscriber subscriber); Task GetSubscriptionAsync(ISubscriber subscriber); - Task GetTaxInfoAsync(ISubscriber subscriber); - Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo); Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount); /// /// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager. @@ -68,7 +64,4 @@ public interface IPaymentService /// Organization Representation used for Inviting Organization Users /// If the organization has Secrets Manager and has the Standalone Stripe Discount Task HasSecretsManagerStandalone(InviteOrganization organization); - Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); - Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId); - } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 4c64abc73e..c887a388bd 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -8,11 +8,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Organizations.Models; -using Bit.Core.Billing.Premium.Commands; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Requests; -using Bit.Core.Billing.Tax.Responses; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -36,8 +32,6 @@ public class StripePaymentService : IPaymentService private readonly Braintree.IBraintreeGateway _btGateway; private readonly IStripeAdapter _stripeAdapter; private readonly IGlobalSettings _globalSettings; - private readonly IFeatureService _featureService; - private readonly ITaxService _taxService; private readonly IPricingClient _pricingClient; public StripePaymentService( @@ -46,8 +40,6 @@ public class StripePaymentService : IPaymentService IStripeAdapter stripeAdapter, Braintree.IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, - IFeatureService featureService, - ITaxService taxService, IPricingClient pricingClient) { _transactionRepository = transactionRepository; @@ -55,8 +47,6 @@ public class StripePaymentService : IPaymentService _stripeAdapter = stripeAdapter; _btGateway = braintreeGateway; _globalSettings = globalSettings; - _featureService = featureService; - _taxService = taxService; _pricingClient = pricingClient; } @@ -705,133 +695,6 @@ public class StripePaymentService : IPaymentService return subscriptionInfo; } - public async Task GetTaxInfoAsync(ISubscriber subscriber) - { - if (subscriber == null || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) - { - return null; - } - - var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, - new CustomerGetOptions { Expand = ["tax_ids"] }); - - if (customer == null) - { - return null; - } - - var address = customer.Address; - var taxId = customer.TaxIds?.FirstOrDefault(); - - // Line1 is required, so if missing we're using the subscriber name, - // see: https://stripe.com/docs/api/customers/create#create_customer-address-line1 - if (address != null && string.IsNullOrWhiteSpace(address.Line1)) - { - address.Line1 = null; - } - - return new TaxInfo - { - TaxIdNumber = taxId?.Value, - TaxIdType = taxId?.Type, - BillingAddressLine1 = address?.Line1, - BillingAddressLine2 = address?.Line2, - BillingAddressCity = address?.City, - BillingAddressState = address?.State, - BillingAddressPostalCode = address?.PostalCode, - BillingAddressCountry = address?.Country, - }; - } - - public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo) - { - if (string.IsNullOrWhiteSpace(subscriber?.GatewayCustomerId) || subscriber.IsUser()) - { - return; - } - - var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, - new CustomerUpdateOptions - { - Address = new AddressOptions - { - Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, - Line2 = taxInfo.BillingAddressLine2, - City = taxInfo.BillingAddressCity, - State = taxInfo.BillingAddressState, - PostalCode = taxInfo.BillingAddressPostalCode, - Country = taxInfo.BillingAddressCountry, - }, - Expand = ["tax_ids"] - }); - - if (customer == null) - { - return; - } - - var taxId = customer.TaxIds?.FirstOrDefault(); - - if (taxId != null) - { - await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); - } - - if (string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) - { - return; - } - - var taxIdType = taxInfo.TaxIdType; - - if (string.IsNullOrWhiteSpace(taxIdType)) - { - taxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); - - if (taxIdType == null) - { - _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", - taxInfo.BillingAddressCountry, - taxInfo.TaxIdNumber); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - } - - try - { - await _stripeAdapter.TaxIdCreateAsync(customer.Id, - new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }); - - if (taxInfo.TaxIdType == StripeConstants.TaxIdType.SpanishNIF) - { - await _stripeAdapter.TaxIdCreateAsync(customer.Id, - new TaxIdCreateOptions - { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{taxInfo.TaxIdNumber}" - }); - } - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - taxInfo.TaxIdNumber, - taxInfo.BillingAddressCountry); - throw new BadRequestException("billingInvalidTaxIdError"); - default: - _logger.LogError(e, - "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", - taxInfo.TaxIdNumber, - taxInfo.BillingAddressCountry, - customer.Id); - throw new BadRequestException("billingTaxIdCreationError"); - } - } - } - public async Task AddSecretsManagerToSubscription( Organization org, StaticStore.Plan plan, @@ -909,309 +772,6 @@ public class StripePaymentService : IPaymentService } } - [Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")] - public async Task PreviewInvoiceAsync( - PreviewIndividualInvoiceRequestBody parameters, - string gatewayCustomerId, - string gatewaySubscriptionId) - { - var premiumPlan = await _pricingClient.GetAvailablePremiumPlan(); - - var options = new InvoiceCreatePreviewOptions - { - AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, }, - Currency = "usd", - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = - [ - new InvoiceSubscriptionDetailsItemOptions - { - Quantity = 1, - Plan = premiumPlan.Seat.StripePriceId - }, - - new InvoiceSubscriptionDetailsItemOptions - { - Quantity = parameters.PasswordManager.AdditionalStorage, - Plan = premiumPlan.Storage.StripePriceId - } - ] - }, - CustomerDetails = new InvoiceCustomerDetailsOptions - { - Address = new AddressOptions - { - PostalCode = parameters.TaxInformation.PostalCode, - Country = parameters.TaxInformation.Country, - } - }, - }; - - if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId)) - { - var taxIdType = _taxService.GetStripeTaxCode( - options.CustomerDetails.Address.Country, - parameters.TaxInformation.TaxId); - - if (taxIdType == null) - { - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - } - - options.CustomerDetails.TaxIds = - [ - new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId } - ]; - - if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) - { - options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions - { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{parameters.TaxInformation.TaxId}" - }); - } - } - - if (!string.IsNullOrWhiteSpace(gatewayCustomerId)) - { - var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); - - if (gatewayCustomer.Discount != null) - { - options.Discounts = [new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id }]; - } - } - - if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId)) - { - var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - - if (gatewaySubscription?.Discounts is { Count: > 0 }) - { - options.Discounts = gatewaySubscription.Discounts.Select(x => new InvoiceDiscountOptions { Coupon = x.Coupon.Id }).ToList(); - } - } - - if (options.Discounts is { Count: > 0 }) - { - options.Discounts = options.Discounts.DistinctBy(invoiceDiscountOptions => invoiceDiscountOptions.Coupon).ToList(); - } - - try - { - var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - - var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount); - - var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 - ? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() - : 0M; - - var result = new PreviewInvoiceResponseModel( - effectiveTaxRate, - invoice.TotalExcludingTax.ToMajor() ?? 0, - tax.ToMajor(), - invoice.Total.ToMajor()); - return result; - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - default: - _logger.LogError(e, - "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvoiceError"); - } - } - } - - public async Task PreviewInvoiceAsync( - PreviewOrganizationInvoiceRequestBody parameters, - string gatewayCustomerId, - string gatewaySubscriptionId) - { - var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan); - var isSponsored = parameters.PasswordManager.SponsoredPlan.HasValue; - - var options = new InvoiceCreatePreviewOptions - { - Currency = "usd", - SubscriptionDetails = new InvoiceSubscriptionDetailsOptions - { - Items = - [ - new InvoiceSubscriptionDetailsItemOptions - { - Quantity = parameters.PasswordManager.AdditionalStorage, - Plan = plan.PasswordManager.StripeStoragePlanId - } - ] - }, - CustomerDetails = new InvoiceCustomerDetailsOptions - { - Address = new AddressOptions - { - PostalCode = parameters.TaxInformation.PostalCode, - Country = parameters.TaxInformation.Country, - } - }, - }; - - if (isSponsored) - { - var sponsoredPlan = SponsoredPlans.Get(parameters.PasswordManager.SponsoredPlan.Value); - options.SubscriptionDetails.Items.Add( - new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId } - ); - } - else - { - if (plan.PasswordManager.HasAdditionalSeatsOption) - { - options.SubscriptionDetails.Items.Add( - new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId } - ); - } - else - { - options.SubscriptionDetails.Items.Add( - new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId } - ); - } - - if (plan.SupportsSecretsManager) - { - if (plan.SecretsManager.HasAdditionalSeatsOption) - { - options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions - { - Quantity = parameters.SecretsManager?.Seats ?? 0, - Plan = plan.SecretsManager.StripeSeatPlanId - }); - } - - if (plan.SecretsManager.HasAdditionalServiceAccountOption) - { - options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions - { - Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0, - Plan = plan.SecretsManager.StripeServiceAccountPlanId - }); - } - } - } - - if (!string.IsNullOrWhiteSpace(parameters.TaxInformation.TaxId)) - { - var taxIdType = _taxService.GetStripeTaxCode( - options.CustomerDetails.Address.Country, - parameters.TaxInformation.TaxId); - - if (taxIdType == null) - { - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingTaxIdTypeInferenceError"); - } - - options.CustomerDetails.TaxIds = - [ - new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId } - ]; - - if (taxIdType == StripeConstants.TaxIdType.SpanishNIF) - { - options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions - { - Type = StripeConstants.TaxIdType.EUVAT, - Value = $"ES{parameters.TaxInformation.TaxId}" - }); - } - } - - Customer gatewayCustomer = null; - - if (!string.IsNullOrWhiteSpace(gatewayCustomerId)) - { - gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId); - - if (gatewayCustomer.Discount != null) - { - options.Discounts = - [ - new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id } - ]; - } - } - - if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId)) - { - var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId); - - if (gatewaySubscription?.Discounts != null) - { - options.Discounts = gatewaySubscription.Discounts - .Select(discount => new InvoiceDiscountOptions { Coupon = discount.Coupon.Id }).ToList(); - } - } - - options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; - if (parameters.PasswordManager.Plan.IsBusinessProductTierType() && - parameters.TaxInformation.Country != Constants.CountryAbbreviations.UnitedStates) - { - options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse; - } - - try - { - var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options); - - var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount); - - var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0 - ? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor() - : 0M; - - var result = new PreviewInvoiceResponseModel( - effectiveTaxRate, - invoice.TotalExcludingTax.ToMajor() ?? 0, - tax.ToMajor(), - invoice.Total.ToMajor()); - return result; - } - catch (StripeException e) - { - switch (e.StripeError.Code) - { - case StripeConstants.ErrorCodes.TaxIdInvalid: - _logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvalidTaxIdError"); - default: - _logger.LogError(e, - "Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.", - parameters.TaxInformation.TaxId, - parameters.TaxInformation.Country); - throw new BadRequestException("billingPreviewInvoiceError"); - } - } - } - private PaymentMethod GetLatestCardPaymentMethod(string customerId) { var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging( diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index f59fce4011..b7349c09d9 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,5 +1,4 @@ using Bit.Api.Billing.Controllers; -using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -11,8 +10,6 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Providers.Entities; using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.Api; using Bit.Core.Models.BitStripe; @@ -521,49 +518,4 @@ public class ProviderBillingControllerTests } #endregion - - #region UpdateTaxInformationAsync - - [Theory, BitAutoData] - public async Task UpdateTaxInformation_NoCountry_BadRequest( - Provider provider, - TaxInformationRequestBody requestBody, - SutProvider sutProvider) - { - ConfigureStableProviderAdminInputs(provider, sutProvider); - - requestBody.Country = null; - - var result = await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody); - - Assert.IsType>(result); - - var response = (BadRequest)result; - - Assert.Equal("Country and postal code are required to update your tax information.", response.Value.Message); - } - - [Theory, BitAutoData] - public async Task UpdateTaxInformation_Ok( - Provider provider, - TaxInformationRequestBody requestBody, - SutProvider sutProvider) - { - ConfigureStableProviderAdminInputs(provider, sutProvider); - - await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody); - - await sutProvider.GetDependency().Received(1).UpdateTaxInformation( - provider, Arg.Is( - options => - options.Country == requestBody.Country && - options.PostalCode == requestBody.PostalCode && - options.TaxId == requestBody.TaxId && - options.Line1 == requestBody.Line1 && - options.Line2 == requestBody.Line2 && - options.City == requestBody.City && - options.State == requestBody.State)); - } - - #endregion } diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 2569ffff00..50fb160754 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -328,157 +328,6 @@ public class SubscriberServiceTests #endregion - #region GetPaymentMethod - - [Theory, BitAutoData] - public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException( - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.GetPaymentSource(null)); - - [Theory, BitAutoData] - public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization, - SutProvider sutProvider) - { - // Arrange - // Stripe reports balance in cents as a negative number for credit - const int stripeAccountBalance = -593; // $5.93 credit (negative cents) - const decimal creditAmount = 5.93M; // Same value in dollars - - - var customer = new Customer - { - Balance = stripeAccountBalance, - Subscriptions = new StripeList() - { - Data = - [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] - }, - InvoiceSettings = new CustomerInvoiceSettings - { - DefaultPaymentMethod = new PaymentMethod - { - Type = StripeConstants.PaymentMethodTypes.USBankAccount, - UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } - } - } - }; - sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, - Arg.Is(options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") - && options.Expand.Contains("subscriptions") - && options.Expand.Contains("tax_ids"))) - .Returns(customer); - - // Act - var result = await sutProvider.Sut.GetPaymentMethod(organization); - - // Assert - Assert.NotNull(result); - Assert.Equal(creditAmount, result.AccountCredit); - await sutProvider.GetDependency().Received(1).CustomerGetAsync( - organization.GatewayCustomerId, - Arg.Is(options => - options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") && - options.Expand.Contains("subscriptions") && - options.Expand.Contains("tax_ids"))); - - } - - [Theory, BitAutoData] - public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount( - Organization organization, SutProvider sutProvider) - { - // Arrange - const int stripeAccountBalance = 0; - - var customer = new Customer - { - Balance = stripeAccountBalance, - Subscriptions = new StripeList() - { - Data = - [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] - }, - InvoiceSettings = new CustomerInvoiceSettings - { - DefaultPaymentMethod = new PaymentMethod - { - Type = StripeConstants.PaymentMethodTypes.USBankAccount, - UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } - } - } - }; - sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, - Arg.Is(options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") - && options.Expand.Contains("subscriptions") - && options.Expand.Contains("tax_ids"))) - .Returns(customer); - - // Act - var result = await sutProvider.Sut.GetPaymentMethod(organization); - - // Assert - Assert.NotNull(result); - Assert.Equal(0, result.AccountCredit); - await sutProvider.GetDependency().Received(1).CustomerGetAsync( - organization.GatewayCustomerId, - Arg.Is(options => - options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") && - options.Expand.Contains("subscriptions") && - options.Expand.Contains("tax_ids"))); - } - - [Theory, BitAutoData] - public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount( - Organization organization, SutProvider sutProvider) - { - // Arrange - const int stripeAccountBalance = 593; // $5.93 charge balance - const decimal accountBalance = -5.93M; // account balance - var customer = new Customer - { - Balance = stripeAccountBalance, - Subscriptions = new StripeList() - { - Data = - [new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }] - }, - InvoiceSettings = new CustomerInvoiceSettings - { - DefaultPaymentMethod = new PaymentMethod - { - Type = StripeConstants.PaymentMethodTypes.USBankAccount, - UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" } - } - } - }; - sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, - Arg.Is(options => options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") - && options.Expand.Contains("subscriptions") - && options.Expand.Contains("tax_ids"))) - .Returns(customer); - - // Act - var result = await sutProvider.Sut.GetPaymentMethod(organization); - - // Assert - Assert.NotNull(result); - Assert.Equal(accountBalance, result.AccountCredit); - await sutProvider.GetDependency().Received(1).CustomerGetAsync( - organization.GatewayCustomerId, - Arg.Is(options => - options.Expand.Contains("default_source") && - options.Expand.Contains("invoice_settings.default_payment_method") && - options.Expand.Contains("subscriptions") && - options.Expand.Contains("tax_ids"))); - - } - #endregion - #region GetPaymentSource [Theory, BitAutoData] @@ -889,65 +738,6 @@ public class SubscriberServiceTests } #endregion - #region GetTaxInformation - - [Theory, BitAutoData] - public async Task GetTaxInformation_NullSubscriber_ThrowsArgumentNullException( - SutProvider sutProvider) => - await Assert.ThrowsAsync(() => sutProvider.Sut.GetTaxInformation(null)); - - [Theory, BitAutoData] - public async Task GetTaxInformation_NullAddress_ReturnsNull( - Organization organization, - SutProvider sutProvider) - { - sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) - .Returns(new Customer()); - - var taxInformation = await sutProvider.Sut.GetTaxInformation(organization); - - Assert.Null(taxInformation); - } - - [Theory, BitAutoData] - public async Task GetTaxInformation_Success( - Organization organization, - SutProvider sutProvider) - { - var address = new Address - { - Country = "US", - PostalCode = "12345", - Line1 = "123 Example St.", - Line2 = "Unit 1", - City = "Example Town", - State = "NY" - }; - - sutProvider.GetDependency().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any()) - .Returns(new Customer - { - Address = address, - TaxIds = new StripeList - { - Data = [new TaxId { Value = "tax_id" }] - } - }); - - var taxInformation = await sutProvider.Sut.GetTaxInformation(organization); - - Assert.NotNull(taxInformation); - Assert.Equal(address.Country, taxInformation.Country); - Assert.Equal(address.PostalCode, taxInformation.PostalCode); - Assert.Equal("tax_id", taxInformation.TaxId); - Assert.Equal(address.Line1, taxInformation.Line1); - Assert.Equal(address.Line2, taxInformation.Line2); - Assert.Equal(address.City, taxInformation.City); - Assert.Equal(address.State, taxInformation.State); - } - - #endregion - #region RemovePaymentMethod [Theory, BitAutoData] public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException( @@ -1844,48 +1634,6 @@ public class SubscriberServiceTests #endregion - #region VerifyBankAccount - - [Theory, BitAutoData] - public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException( - Provider provider, - SutProvider sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, "")); - - [Theory, BitAutoData] - public async Task VerifyBankAccount_MakesCorrectInvocations( - Provider provider, - SutProvider sutProvider) - { - const string descriptorCode = "SM1234"; - - var setupIntent = new SetupIntent - { - Id = "setup_intent_id", - PaymentMethodId = "payment_method_id" - }; - - sutProvider.GetDependency().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id); - - var stripeAdapter = sutProvider.GetDependency(); - - stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent); - - await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode); - - await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id, - Arg.Is( - options => options.DescriptorCode == descriptorCode)); - - await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId, - Arg.Is( - options => options.Customer == provider.GatewayCustomerId)); - - await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is( - options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId)); - } - - #endregion - #region IsValidGatewayCustomerIdAsync [Theory, BitAutoData] diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index dc62af0872..8f556be57a 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,11 +1,7 @@ using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Requests; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Test.Billing.Mocks.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -17,506 +13,6 @@ namespace Bit.Core.Test.Services; [SutProviderCustomize] public class StripePaymentServiceTests { - [Theory] - [BitAutoData] - public async Task - PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage( - SutProvider sutProvider) - { - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = - new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - AdditionalStorage = 0 - }, - TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } - }; - - sutProvider.GetDependency() - .InvoiceCreatePreviewAsync(Arg.Is(p => - p.Currency == "usd" && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripePlanId && - x.Quantity == 1) && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && - x.Quantity == 0))) - .Returns(new Invoice - { - TotalExcludingTax = 4000, - TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], - Total = 4800 - }); - - var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - Assert.Equal(8M, actual.TaxAmount); - Assert.Equal(48M, actual.TotalAmount); - Assert.Equal(40M, actual.TaxableBaseAmount); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage( - SutProvider sutProvider) - { - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = - new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - AdditionalStorage = 1 - }, - TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } - }; - - sutProvider.GetDependency() - .InvoiceCreatePreviewAsync(Arg.Is(p => - p.Currency == "usd" && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripePlanId && - x.Quantity == 1) && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && - x.Quantity == 1))) - .Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 }); - - var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - Assert.Equal(8M, actual.TaxAmount); - Assert.Equal(48M, actual.TotalAmount); - Assert.Equal(40M, actual.TaxableBaseAmount); - } - - [Theory] - [BitAutoData] - public async Task - PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage( - SutProvider sutProvider) - { - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, - AdditionalStorage = 0 - }, - TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } - }; - - sutProvider.GetDependency() - .InvoiceCreatePreviewAsync(Arg.Is(p => - p.Currency == "usd" && - p.SubscriptionDetails.Items.Any(x => - x.Plan == "2021-family-for-enterprise-annually" && - x.Quantity == 1) && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && - x.Quantity == 0))) - .Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 }); - - var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - Assert.Equal(0M, actual.TaxAmount); - Assert.Equal(0M, actual.TotalAmount); - Assert.Equal(0M, actual.TaxableBaseAmount); - } - - [Theory] - [BitAutoData] - public async Task - PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage( - SutProvider sutProvider) - { - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually, - SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise, - AdditionalStorage = 1 - }, - TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" } - }; - - sutProvider.GetDependency() - .InvoiceCreatePreviewAsync(Arg.Is(p => - p.Currency == "usd" && - p.SubscriptionDetails.Items.Any(x => - x.Plan == "2021-family-for-enterprise-annually" && - x.Quantity == 1) && - p.SubscriptionDetails.Items.Any(x => - x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId && - x.Quantity == 1))) - .Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 }); - - var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - Assert.Equal(0.08M, actual.TaxAmount); - Assert.Equal(4.08M, actual.TotalAmount); - Assert.Equal(4M, actual.TaxableBaseAmount); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) - { - // Arrange - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "US", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) - { - // Arrange - var plan = new EnterprisePlan(true); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) - .Returns(plan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.EnterpriseAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "US", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) - { - // Arrange - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider sutProvider) - { - // Arrange - var plan = new EnterprisePlan(true); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) - .Returns(plan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.EnterpriseAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) - { - // Arrange - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "US", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider sutProvider) - { - // Arrange - var plan = new EnterprisePlan(true); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) - .Returns(plan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.EnterpriseAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "US", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider sutProvider) - { - // Arrange - var familiesPlan = new FamiliesPlan(); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.FamiliesAnnually)) - .Returns(familiesPlan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.FamiliesAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == null - )); - } - - [Theory] - [BitAutoData] - public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider sutProvider) - { - // Arrange - var plan = new EnterprisePlan(true); - sutProvider.GetDependency() - .GetPlanOrThrow(Arg.Is(p => p == PlanType.EnterpriseAnnually)) - .Returns(plan); - - var parameters = new PreviewOrganizationInvoiceRequestBody - { - PasswordManager = new OrganizationPasswordManagerRequestModel - { - Plan = PlanType.EnterpriseAnnually - }, - TaxInformation = new TaxInformationRequestModel - { - Country = "FR", - PostalCode = "12345" - } - }; - - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter - .InvoiceCreatePreviewAsync(Arg.Any()) - .Returns(new Invoice - { - TotalExcludingTax = 400, - TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], - Total = 408 - }); - - // Act - await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null); - - // Assert - await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is(options => - options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse - )); - } - [Theory] [BitAutoData] public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer(