From 36a93b9ab26ad7a659693d40bab0a288adecd75c Mon Sep 17 00:00:00 2001 From: Alex Morask Date: Fri, 5 Dec 2025 11:05:57 -0600 Subject: [PATCH] MOAR DELETE --- .../Requests/TaxInformationRequestBody.cs | 31 -- .../TokenizedPaymentSourceRequestBody.cs | 25 - .../UpdatePaymentMethodRequestBody.cs | 15 - .../Requests/VerifyBankAccountRequestBody.cs | 12 - .../Responses/BillingPaymentResponseModel.cs | 20 - .../Models/Responses/PaymentMethodResponse.cs | 18 - .../Models/Responses/PaymentSourceResponse.cs | 16 - .../Responses/TaxInformationResponse.cs | 23 - src/Core/Services/IPaymentService.cs | 5 - .../Implementations/StripePaymentService.cs | 440 --------------- .../Services/StripePaymentServiceTests.cs | 504 ------------------ 11 files changed, 1109 deletions(-) delete mode 100644 src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs delete mode 100644 src/Api/Billing/Models/Requests/TokenizedPaymentSourceRequestBody.cs delete mode 100644 src/Api/Billing/Models/Requests/UpdatePaymentMethodRequestBody.cs delete mode 100644 src/Api/Billing/Models/Requests/VerifyBankAccountRequestBody.cs delete mode 100644 src/Api/Billing/Models/Responses/BillingPaymentResponseModel.cs delete mode 100644 src/Api/Billing/Models/Responses/PaymentMethodResponse.cs delete mode 100644 src/Api/Billing/Models/Responses/PaymentSourceResponse.cs delete mode 100644 src/Api/Billing/Models/Responses/TaxInformationResponse.cs 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/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index e7e848bcba..98506f46ef 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -44,8 +44,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 +66,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/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(