From e77430823c91cf66873a26942b97129d5f089d55 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Tue, 19 Nov 2024 13:48:07 +0100 Subject: [PATCH] Sales tax id type detection --- .../Billing/TaxServiceTests.cs | 151 +++ .../OrganizationBillingController.cs | 5 +- .../Controllers/OrganizationsController.cs | 21 +- .../Controllers/ProviderBillingController.cs | 1 + .../Billing/Controllers/StripeController.cs | 7 +- .../Requests/TaxInformationRequestBody.cs | 2 + .../ExpandedTaxInfoUpdateRequestModel.cs | 3 +- .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Billing/Models/Sales/OrganizationSale.cs | 1 + src/Core/Billing/Models/TaxIdType.cs | 6 +- src/Core/Billing/Models/TaxInformation.cs | 160 +-- src/Core/Billing/Services/ITaxService.cs | 10 + .../OrganizationBillingService.cs | 35 +- .../PremiumUserBillingService.cs | 37 +- .../Implementations/SubscriberService.cs | 55 +- src/Core/Billing/Services/TaxService.cs | 907 ++++++++++++++++++ src/Core/Billing/Utilities.cs | 1 + src/Core/Models/Business/TaxInfo.cs | 209 +--- .../Implementations/StripePaymentService.cs | 40 +- src/Core/Utilities/StaticStore.cs | 291 ------ .../Services/SubscriberServiceTests.cs | 3 +- 21 files changed, 1247 insertions(+), 699 deletions(-) create mode 100644 bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs create mode 100644 src/Core/Billing/Services/ITaxService.cs create mode 100644 src/Core/Billing/Services/TaxService.cs diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs new file mode 100644 index 000000000..3995fb9de --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs @@ -0,0 +1,151 @@ +using Bit.Core.Billing.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Commercial.Core.Test.Billing; + +[SutProviderCustomize] +public class TaxServiceTests +{ + [Theory] + [BitAutoData("AD", "A-123456-Z", "ad_nrt")] + [BitAutoData("AD", "A123456Z", "ad_nrt")] + [BitAutoData("AR", "20-12345678-9", "ar_cuit")] + [BitAutoData("AR", "20123456789", "ar_cuit")] + [BitAutoData("AU", "01259983598", "au_abn")] + [BitAutoData("AU", "123456789123", "au_arn")] + [BitAutoData("AT", "ATU12345678", "eu_vat")] + [BitAutoData("BH", "123456789012345", "bh_vat")] + [BitAutoData("BY", "123456789", "by_tin")] + [BitAutoData("BE", "BE0123456789", "eu_vat")] + [BitAutoData("BO", "123456789", "bo_tin")] + [BitAutoData("BR", "01.234.456/5432-10", "br_cnpj")] + [BitAutoData("BR", "01234456543210", "br_cnpj")] + [BitAutoData("BR", "123.456.789-87", "br_cpf")] + [BitAutoData("BR", "12345678987", "br_cpf")] + [BitAutoData("BG", "123456789", "bg_uic")] + [BitAutoData("BG", "BG012100705", "eu_vat")] + [BitAutoData("CA", "100728494", "ca_bn")] + [BitAutoData("CA", "123456789RT0001", "ca_gst_hst")] + [BitAutoData("CA", "PST-1234-1234", "ca_pst_bc")] + [BitAutoData("CA", "123456-7", "ca_pst_mb")] + [BitAutoData("CA", "1234567", "ca_pst_sk")] + [BitAutoData("CA", "1234567890TQ1234", "ca_qst")] + [BitAutoData("CL", "11.121.326-1", "cl_tin")] + [BitAutoData("CL", "11121326-1", "cl_tin")] + [BitAutoData("CL", "23.121.326-K", "cl_tin")] + [BitAutoData("CL", "43651326-K", "cl_tin")] + [BitAutoData("CN", "123456789012345678", "cn_tin")] + [BitAutoData("CN", "123456789012345", "cn_tin")] + [BitAutoData("CO", "123.456.789-0", "co_nit")] + [BitAutoData("CO", "1234567890", "co_nit")] + [BitAutoData("CR", "1-234-567890", "cr_tin")] + [BitAutoData("CR", "1234567890", "cr_tin")] + [BitAutoData("HR", "HR12345678912", "eu_vat")] + [BitAutoData("HR", "12345678901", "hr_oib")] + [BitAutoData("CY", "CY12345678X", "eu_vat")] + [BitAutoData("CZ", "CZ12345678", "eu_vat")] + [BitAutoData("DK", "DK12345678", "eu_vat")] + [BitAutoData("DO", "123-4567890-1", "do_rcn")] + [BitAutoData("DO", "12345678901", "do_rcn")] + [BitAutoData("EC", "1234567890001", "ec_ruc")] + [BitAutoData("EG", "123456789", "eg_tin")] + [BitAutoData("SV", "1234-567890-123-4", "sv_nit")] + [BitAutoData("SV", "12345678901234", "sv_nit")] + [BitAutoData("EE", "EE123456789", "eu_vat")] + [BitAutoData("EU", "EU123456789", "eu_oss_vat")] + [BitAutoData("FI", "FI12345678", "eu_vat")] + [BitAutoData("FR", "FR12345678901", "eu_vat")] + [BitAutoData("GE", "123456789", "ge_vat")] + [BitAutoData("DE", "1234567890", "de_stn")] + [BitAutoData("DE", "DE123456789", "eu_vat")] + [BitAutoData("GR", "EL123456789", "eu_vat")] + [BitAutoData("HK", "12345678", "hk_br")] + [BitAutoData("HU", "HU12345678", "eu_vat")] + [BitAutoData("HU", "12345678-1-23", "hu_tin")] + [BitAutoData("HU", "12345678123", "hu_tin")] + [BitAutoData("IS", "123456", "is_vat")] + [BitAutoData("IN", "12ABCDE1234F1Z5", "in_gst")] + [BitAutoData("IN", "12ABCDE3456FGZH", "in_gst")] + [BitAutoData("ID", "012.345.678.9-012.345", "id_npwp")] + [BitAutoData("ID", "0123456789012345", "id_npwp")] + [BitAutoData("IE", "IE1234567A", "eu_vat")] + [BitAutoData("IE", "IE1234567AB", "eu_vat")] + [BitAutoData("IL", "000012345", "il_vat")] + [BitAutoData("IL", "123456789", "il_vat")] + [BitAutoData("IT", "IT12345678901", "eu_vat")] + [BitAutoData("JP", "1234567890123", "jp_cn")] + [BitAutoData("JP", "12345", "jp_rn")] + [BitAutoData("KZ", "123456789012", "kz_bin")] + [BitAutoData("KE", "P000111111A", "ke_pin")] + [BitAutoData("LV", "LV12345678912", "eu_vat")] + [BitAutoData("LI", "CHE123456789", "li_uid")] + [BitAutoData("LI", "12345", "li_vat")] + [BitAutoData("LT", "LT123456789123", "eu_vat")] + [BitAutoData("LU", "LU12345678", "eu_vat")] + [BitAutoData("MY", "12345678", "my_frp")] + [BitAutoData("MY", "C 1234567890", "my_itn")] + [BitAutoData("MY", "C1234567890", "my_itn")] + [BitAutoData("MY", "A12-3456-78912345", "my_sst")] + [BitAutoData("MY", "A12345678912345", "my_sst")] + [BitAutoData("MT", "MT12345678", "eu_vat")] + [BitAutoData("MX", "ABC010203AB9", "mx_rfc")] + [BitAutoData("MD", "1003600", "md_vat")] + [BitAutoData("MA", "12345678", "ma_vat")] + [BitAutoData("NL", "NL123456789B12", "eu_vat")] + [BitAutoData("NZ", "123456789", "nz_gst")] + [BitAutoData("NG", "12345678-0001", "ng_tin")] + [BitAutoData("NO", "123456789MVA", "no_vat")] + [BitAutoData("NO", "1234567", "no_voec")] + [BitAutoData("OM", "OM1234567890", "om_vat")] + [BitAutoData("PE", "12345678901", "pe_ruc")] + [BitAutoData("PH", "123456789012", "ph_tin")] + [BitAutoData("PL", "PL1234567890", "eu_vat")] + [BitAutoData("PT", "PT123456789", "eu_vat")] + [BitAutoData("RO", "RO1234567891", "eu_vat")] + [BitAutoData("RO", "1234567890123", "ro_tin")] + [BitAutoData("RU", "1234567891", "ru_inn")] + [BitAutoData("RU", "123456789", "ru_kpp")] + [BitAutoData("SA", "123456789012345", "sa_vat")] + [BitAutoData("RS", "123456789", "rs_pib")] + [BitAutoData("SG", "M12345678X", "sg_gst")] + [BitAutoData("SG", "123456789F", "sg_uen")] + [BitAutoData("SK", "SK1234567891", "eu_vat")] + [BitAutoData("SI", "SI12345678", "eu_vat")] + [BitAutoData("SI", "12345678", "si_tin")] + [BitAutoData("ZA", "4123456789", "za_vat")] + [BitAutoData("KR", "123-45-67890", "kr_brn")] + [BitAutoData("KR", "1234567890", "kr_brn")] + [BitAutoData("ES", "A12345678", "es_cif")] + [BitAutoData("ES", "ESX1234567X", "eu_vat")] + [BitAutoData("SE", "SE123456789012", "eu_vat")] + [BitAutoData("CH", "CHE-123.456.789 HR", "ch_uid")] + [BitAutoData("CH", "CHE123456789HR", "ch_uid")] + [BitAutoData("CH", "CHE-123.456.789 MWST", "ch_vat")] + [BitAutoData("CH", "CHE123456789MWST", "ch_vat")] + [BitAutoData("TW", "12345678", "tw_vat")] + [BitAutoData("TH", "1234567890123", "th_vat")] + [BitAutoData("TR", "0123456789", "tr_tin")] + [BitAutoData("UA", "123456789", "ua_vat")] + [BitAutoData("AE", "123456789012345", "ae_trn")] + [BitAutoData("GB", "XI123456789", "eu_vat")] + [BitAutoData("GB", "GB123456789", "gb_vat")] + [BitAutoData("US", "12-3456789", "us_ein")] + [BitAutoData("UY", "123456789012", "uy_ruc")] + [BitAutoData("UZ", "123456789", "uz_tin")] + [BitAutoData("UZ", "123456789012", "uz_vat")] + [BitAutoData("VE", "A-12345678-9", "ve_rif")] + [BitAutoData("VE", "A123456789", "ve_rif")] + [BitAutoData("VN", "1234567890", "vn_tin")] + public void GetStripeTaxCode_WithValidCountryAndTaxId_ReturnsExpectedTaxIdType( + string country, + string taxId, + string expected, + SutProvider sutProvider) + { + var result = sutProvider.Sut.GetStripeTaxCode(country, taxId); + + Assert.Equal(expected, result); + } +} diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index b6a26f240..9459366e6 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -21,7 +21,8 @@ public class OrganizationBillingController( IOrganizationRepository organizationRepository, IPaymentService paymentService, ISubscriberService subscriberService, - IPaymentHistoryService paymentHistoryService) : BaseBillingController + IPaymentHistoryService paymentHistoryService, + ITaxService taxService) : BaseBillingController { [HttpGet("metadata")] public async Task GetMetadataAsync([FromRoute] Guid organizationId) @@ -267,6 +268,8 @@ public class OrganizationBillingController( return Error.NotFound(); } + requestBody.TaxIdType = taxService.GetStripeTaxCode(requestBody.Country, requestBody.TaxId); + var taxInformation = requestBody.ToDomain(); await subscriberService.UpdateTaxInformation(organization, taxInformation); diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 75ae2fb89..510cf670f 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -311,17 +311,16 @@ public class OrganizationsController( 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); + var taxInfo = new TaxInformation( + model.Country, + model.PostalCode, + model.TaxId, + model.TaxIdType, + model.Line1, + model.Line2, + model.City, + model.State); + await subscriberService.UpdateTaxInformation(organization, taxInfo); } /// diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index d2209685a..9fd993330 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -120,6 +120,7 @@ public class ProviderBillingController( requestBody.Country, requestBody.PostalCode, requestBody.TaxId, + requestBody.TaxIdType, requestBody.Line1, requestBody.Line2, requestBody.City, diff --git a/src/Api/Billing/Controllers/StripeController.cs b/src/Api/Billing/Controllers/StripeController.cs index 5931de065..4c5ad6f0f 100644 --- a/src/Api/Billing/Controllers/StripeController.cs +++ b/src/Api/Billing/Controllers/StripeController.cs @@ -1,6 +1,6 @@ using Bit.Api.Billing.Models.Responses; +using Bit.Core.Billing.Services; using Bit.Core.Services; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; @@ -10,7 +10,8 @@ namespace Bit.Api.Billing.Controllers; [Authorize("Application")] public class StripeController( - IStripeAdapter stripeAdapter) : Controller + IStripeAdapter stripeAdapter, + ITaxService taxService) : Controller { [HttpPost] [Route("~/setup-intent/bank-account")] @@ -54,7 +55,7 @@ public class StripeController( [Route("~/tax/id-types")] public IResult GetTaxIdTypes() { - var response = TaxIdTypesResponse.From(StaticStore.SupportedTaxIdTypes); + var response = TaxIdTypesResponse.From(taxService.GetTaxIdTypes()); return TypedResults.Ok(response); } } diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index c5c0fde00..57572d5e8 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -10,6 +10,7 @@ public class TaxInformationRequestBody [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; } @@ -19,6 +20,7 @@ public class TaxInformationRequestBody Country, PostalCode, TaxId, + TaxIdType, Line1, Line2, City, diff --git a/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs b/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs index 7f95d755a..1cf3ab3e0 100644 --- a/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs +++ b/src/Api/Models/Request/ExpandedTaxInfoUpdateRequestModel.cs @@ -4,7 +4,8 @@ namespace Bit.Api.Models.Request; public class ExpandedTaxInfoUpdateRequestModel : TaxInfoUpdateRequestModel { - public string TaxId { 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; } diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index abfceac73..8f2803d92 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -11,6 +11,7 @@ public static class ServiceCollectionExtensions { public static void AddBillingOperations(this IServiceCollection services) { + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index a19c278c6..43852bb32 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -65,6 +65,7 @@ public class OrganizationSale signup.TaxInfo.BillingAddressCountry, signup.TaxInfo.BillingAddressPostalCode, signup.TaxInfo.TaxIdNumber, + signup.TaxInfo.TaxIdType, signup.TaxInfo.BillingAddressLine1, signup.TaxInfo.BillingAddressLine2, signup.TaxInfo.BillingAddressCity, diff --git a/src/Core/Billing/Models/TaxIdType.cs b/src/Core/Billing/Models/TaxIdType.cs index 64c0dc9c5..3fc246d68 100644 --- a/src/Core/Billing/Models/TaxIdType.cs +++ b/src/Core/Billing/Models/TaxIdType.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Models; +using System.Text.RegularExpressions; + +namespace Bit.Core.Billing.Models; public class TaxIdType { @@ -12,6 +14,8 @@ public class TaxIdType /// public string Code { get; set; } + public Regex ValidationExpression { get; set; } + public string Description { get; set; } public string Example { get; set; } diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Models/TaxInformation.cs index 5403f9469..23ed3e5fa 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Models/TaxInformation.cs @@ -1,5 +1,4 @@ using Bit.Core.Models.Business; -using Stripe; namespace Bit.Core.Billing.Models; @@ -7,6 +6,7 @@ public record TaxInformation( string Country, string PostalCode, string TaxId, + string TaxIdType, string Line1, string Line2, string City, @@ -16,165 +16,9 @@ public record TaxInformation( taxInfo.BillingAddressCountry, taxInfo.BillingAddressPostalCode, taxInfo.TaxIdNumber, + taxInfo.TaxIdType, taxInfo.BillingAddressLine1, taxInfo.BillingAddressLine2, taxInfo.BillingAddressCity, taxInfo.BillingAddressState); - - public (AddressOptions, List) GetStripeOptions() - { - var address = new AddressOptions - { - Country = Country, - PostalCode = PostalCode, - Line1 = Line1, - Line2 = Line2, - City = City, - State = State - }; - - var customerTaxIdDataOptionsList = !string.IsNullOrEmpty(TaxId) - ? new List { new() { Type = GetTaxIdType(), Value = TaxId } } - : null; - - return (address, customerTaxIdDataOptionsList); - } - - public string GetTaxIdType() - { - if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId)) - { - return null; - } - - switch (Country.ToUpper()) - { - case "AD": - return "ad_nrt"; - case "AE": - return "ae_trn"; - case "AR": - return "ar_cuit"; - case "AU": - return "au_abn"; - case "BO": - return "bo_tin"; - case "BR": - return "br_cnpj"; - case "CA": - // May break for those in Québec given the assumption of QST - if (State?.Contains("bec") ?? false) - { - return "ca_qst"; - } - return "ca_bn"; - case "CH": - return "ch_vat"; - case "CL": - return "cl_tin"; - case "CN": - return "cn_tin"; - case "CO": - return "co_nit"; - case "CR": - return "cr_tin"; - case "DO": - return "do_rcn"; - case "EC": - return "ec_ruc"; - case "EG": - return "eg_tin"; - case "GE": - return "ge_vat"; - case "ID": - return "id_npwp"; - case "IL": - return "il_vat"; - case "IS": - return "is_vat"; - case "KE": - return "ke_pin"; - case "AT": - case "BE": - case "BG": - case "CY": - case "CZ": - case "DE": - case "DK": - case "EE": - case "ES": - case "FI": - case "FR": - case "GB": - case "GR": - case "HR": - case "HU": - case "IE": - case "IT": - case "LT": - case "LU": - case "LV": - case "MT": - case "NL": - case "PL": - case "PT": - case "RO": - case "SE": - case "SI": - case "SK": - return "eu_vat"; - case "HK": - return "hk_br"; - case "IN": - return "in_gst"; - case "JP": - return "jp_cn"; - case "KR": - return "kr_brn"; - case "LI": - return "li_uid"; - case "MX": - return "mx_rfc"; - case "MY": - return "my_sst"; - case "NO": - return "no_vat"; - case "NZ": - return "nz_gst"; - case "PE": - return "pe_ruc"; - case "PH": - return "ph_tin"; - case "RS": - return "rs_pib"; - case "RU": - return "ru_inn"; - case "SA": - return "sa_vat"; - case "SG": - return "sg_gst"; - case "SV": - return "sv_nit"; - case "TH": - return "th_vat"; - case "TR": - return "tr_tin"; - case "TW": - return "tw_vat"; - case "UA": - return "ua_vat"; - case "US": - return "us_ein"; - case "UY": - return "uy_ruc"; - case "VE": - return "ve_rif"; - case "VN": - return "vn_tin"; - case "ZA": - return "za_vat"; - default: - return null; - } - } } diff --git a/src/Core/Billing/Services/ITaxService.cs b/src/Core/Billing/Services/ITaxService.cs new file mode 100644 index 000000000..f354efd04 --- /dev/null +++ b/src/Core/Billing/Services/ITaxService.cs @@ -0,0 +1,10 @@ +using Bit.Core.Billing.Models; + +namespace Bit.Core.Billing.Services; + +public interface ITaxService +{ + string? GetStripeTaxCode(string country, string taxId); + + IEnumerable GetTaxIdTypes(); +} diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index eadc58962..8b161aa23 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -28,7 +28,8 @@ public class OrganizationBillingService( IOrganizationRepository organizationRepository, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ISubscriberService subscriberService) : IOrganizationBillingService + ISubscriberService subscriberService, + ITaxService taxService) : IOrganizationBillingService { public async Task Finalize(OrganizationSale sale) { @@ -167,14 +168,38 @@ public class OrganizationBillingService( throw new BillingException(); } - var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); - - customerCreateOptions.Address = address; + customerCreateOptions.Address = new AddressOptions + { + Line1 = customerSetup.TaxInformation.Line1, + Line2 = customerSetup.TaxInformation.Line2, + City = customerSetup.TaxInformation.City, + PostalCode = customerSetup.TaxInformation.PostalCode, + State = customerSetup.TaxInformation.State, + Country = customerSetup.TaxInformation.Country, + }; customerCreateOptions.Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - customerCreateOptions.TaxIdData = taxIdData; + + if (customerSetup.TaxInformation.TaxId != null) + { + var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + + if (taxIdType == null) + { + logger.LogWarning("Could not determine tax ID type for organization '{OrganizationID}' in country '{Country}' with tax ID '{TaxID}'.", + organization.Id, + customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + } + + customerCreateOptions.TaxIdData = + [ + new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } + ]; + } var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 92c81dae1..08d844018 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -24,7 +24,8 @@ public class PremiumUserBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository) : IPremiumUserBillingService + IUserRepository userRepository, + ITaxService taxService) : IPremiumUserBillingService { public async Task Finalize(PremiumUserSale sale) { @@ -82,13 +83,19 @@ public class PremiumUserBillingService( throw new BillingException(); } - var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); - var subscriberName = user.SubscriberName(); var customerCreateOptions = new CustomerCreateOptions { - Address = address, + Address = new AddressOptions + { + Line1 = customerSetup.TaxInformation.Line1, + Line2 = customerSetup.TaxInformation.Line2, + City = customerSetup.TaxInformation.City, + PostalCode = customerSetup.TaxInformation.PostalCode, + State = customerSetup.TaxInformation.State, + Country = customerSetup.TaxInformation.Country, + }, Description = user.Name, Email = user.Email, Expand = ["tax"], @@ -113,10 +120,28 @@ public class PremiumUserBillingService( Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately - }, - TaxIdData = taxIdData + } }; + if (customerSetup.TaxInformation.TaxId != null) + { + var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + + if (taxIdType == null) + { + logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + customerSetup.TaxInformation.Country, + customerSetup.TaxInformation.TaxId); + throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + } + + customerCreateOptions.TaxIdData = + [ + new() { Type = taxIdType, Value = customerSetup.TaxInformation.TaxId } + ]; + } + var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; var braintreeCustomerId = ""; diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 9daf95622..ffa43d62d 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -22,7 +22,8 @@ public class SubscriberService( IGlobalSettings globalSettings, ILogger logger, ISetupIntentCache setupIntentCache, - IStripeAdapter stripeAdapter) : ISubscriberService + IStripeAdapter stripeAdapter, + ITaxService taxService) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -616,16 +617,51 @@ public class SubscriberService( await stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); } - var taxIdType = taxInformation.GetTaxIdType(); - - if (!string.IsNullOrWhiteSpace(taxInformation.TaxId) && - !string.IsNullOrWhiteSpace(taxIdType)) + if (string.IsNullOrWhiteSpace(taxInformation.TaxId)) { - await stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions + return; + } + + string taxIdType; + if (string.IsNullOrWhiteSpace(taxInformation.TaxIdType)) + { + taxIdType = taxService.GetStripeTaxCode(taxInformation.Country, + taxInformation.TaxId); + + if (taxIdType == null) { - Type = taxIdType, - Value = taxInformation.TaxId, - }); + logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + taxInformation.Country, + taxInformation.TaxId); + throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + } + } + else + { + taxIdType = taxInformation.TaxIdType; + } + + try + { + await stripeAdapter.TaxIdCreateAsync(customer.Id, + new TaxIdCreateOptions { Type = taxIdType, Value = taxInformation.TaxId }); + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case "tax_id_invalid": + logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", + taxInformation.TaxId, + taxInformation.Country); + throw new Exceptions.BadRequestException("billingInvalidTaxIdError"); + default: + logger.LogError(e, "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", + taxInformation.TaxId, + taxInformation.Country, + customer.Id); + throw new Exceptions.BadRequestException("billingTaxIdCreationError"); + } } } @@ -756,6 +792,7 @@ public class SubscriberService( customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, + customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Billing/Services/TaxService.cs b/src/Core/Billing/Services/TaxService.cs new file mode 100644 index 000000000..5ff7bc3c6 --- /dev/null +++ b/src/Core/Billing/Services/TaxService.cs @@ -0,0 +1,907 @@ +using System.Text.RegularExpressions; +using Bit.Core.Billing.Models; + +namespace Bit.Core.Billing.Services; + +public class TaxService : ITaxService +{ + /// + /// Retrieves a list of supported tax ID types for customers. + /// + /// Compiled list from Stripe + private static readonly IEnumerable _taxIdTypes = + [ + new() + { + Country = "AD", + Code = "ad_nrt", + Description = "Andorran NRT number", + Example = "A-123456-Z", + ValidationExpression = new Regex("^([A-Z]{1})-?([0-9]{6})-?([A-Z]{1})$") + }, + new() + { + Country = "AR", + Code = "ar_cuit", + Description = "Argentinian tax ID number", + Example = "12-34567890-1", + ValidationExpression = new Regex("^([0-9]{2})-?([0-9]{8})-?([0-9]{1})$") + }, + new() + { + Country = "AU", + Code = "au_abn", + Description = "Australian Business Number (AU ABN)", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "AU", + Code = "au_arn", + Description = "Australian Taxation Office Reference Number", + Example = "123456789123", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "AT", + Code = "eu_vat", + Description = "European VAT number (Austria)", + Example = "ATU12345678", + ValidationExpression = new Regex("^ATU[0-9]{8}$") + }, + new() + { + Country = "BH", + Code = "bh_vat", + Description = "Bahraini VAT Number", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "BY", + Code = "by_tin", + Description = "Belarus TIN Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BE", + Code = "eu_vat", + Description = "European VAT number (Belgium)", + Example = "BE0123456789", + ValidationExpression = new Regex("^BE[0-9]{10}$") + }, + new() + { + Country = "BO", + Code = "bo_tin", + Description = "Bolivian tax ID", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BR", + Code = "br_cnpj", + Description = "Brazilian CNPJ number", + Example = "01.234.456/5432-10", + ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}/?[0-9]{4}-?[0-9]{2}$") + }, + new() + { + Country = "BR", + Code = "br_cpf", + Description = "Brazilian CPF number", + Example = "123.456.789-87", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{2}$") + }, + new() + { + Country = "BG", + Code = "bg_uic", + Description = "Bulgaria Unified Identification Code", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "BG", + Code = "eu_vat", + Description = "European VAT number (Bulgaria)", + Example = "BG0123456789", + ValidationExpression = new Regex("^BG[0-9]{9,10}$") + }, + new() + { + Country = "CA", + Code = "ca_bn", + Description = "Canadian BN", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "CA", + Code = "ca_gst_hst", + Description = "Canadian GST/HST number", + Example = "123456789RT0002", + ValidationExpression = new Regex("^[0-9]{9}RT[0-9]{4}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_bc", + Description = "Canadian PST number (British Columbia)", + Example = "PST-1234-5678", + ValidationExpression = new Regex("^PST-[0-9]{4}-[0-9]{4}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_mb", + Description = "Canadian PST number (Manitoba)", + Example = "123456-7", + ValidationExpression = new Regex("^[0-9]{6}-[0-9]{1}$") + }, + new() + { + Country = "CA", + Code = "ca_pst_sk", + Description = "Canadian PST number (Saskatchewan)", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "CA", + Code = "ca_qst", + Description = "Canadian QST number (Québec)", + Example = "1234567890TQ1234", + ValidationExpression = new Regex("^[0-9]{10}TQ[0-9]{4}$") + }, + new() + { + Country = "CL", + Code = "cl_tin", + Description = "Chilean TIN", + Example = "12.345.678-K", + ValidationExpression = new Regex("^[0-9]{2}.?[0-9]{3}.?[0-9]{3}-?[0-9A-Z]{1}$") + }, + new() + { + Country = "CN", + Code = "cn_tin", + Description = "Chinese tax ID", + Example = "123456789012345678", + ValidationExpression = new Regex("^[0-9]{15,18}$") + }, + new() + { + Country = "CO", + Code = "co_nit", + Description = "Colombian NIT number", + Example = "123.456.789-0", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}-?[0-9]{1}$") + }, + new() + { + Country = "CR", + Code = "cr_tin", + Description = "Costa Rican tax ID", + Example = "1-234-567890", + ValidationExpression = new Regex("^[0-9]{1}-?[0-9]{3}-?[0-9]{6}$") + }, + new() + { + Country = "HR", + Code = "eu_vat", + Description = "European VAT number (Croatia)", + Example = "HR12345678912", + ValidationExpression = new Regex("^HR[0-9]{11}$") + }, + new() + { + Country = "HR", + Code = "hr_oib", + Description = "Croatian Personal Identification Number", + Example = "12345678901", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "CY", + Code = "eu_vat", + Description = "European VAT number (Cyprus)", + Example = "CY12345678X", + ValidationExpression = new Regex("^CY[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "CZ", + Code = "eu_vat", + Description = "European VAT number (Czech Republic)", + Example = "CZ12345678", + ValidationExpression = new Regex("^CZ[0-9]{8,10}$") + }, + new() + { + Country = "DK", + Code = "eu_vat", + Description = "European VAT number (Denmark)", + Example = "DK12345678", + ValidationExpression = new Regex("^DK[0-9]{8}$") + }, + new() + { + Country = "DO", + Code = "do_rcn", + Description = "Dominican RCN number", + Example = "123-4567890-1", + ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{7}-?[0-9]{1}$") + }, + new() + { + Country = "EC", + Code = "ec_ruc", + Description = "Ecuadorian RUC number", + Example = "1234567890001", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "EG", + Code = "eg_tin", + Description = "Egyptian Tax Identification Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + + new() + { + Country = "SV", + Code = "sv_nit", + Description = "El Salvadorian NIT number", + Example = "1234-567890-123-4", + ValidationExpression = new Regex("^[0-9]{4}-?[0-9]{6}-?[0-9]{3}-?[0-9]{1}$") + }, + + new() + { + Country = "EE", + Code = "eu_vat", + Description = "European VAT number (Estonia)", + Example = "EE123456789", + ValidationExpression = new Regex("^EE[0-9]{9}$") + }, + + new() + { + Country = "EU", + Code = "eu_oss_vat", + Description = "European One Stop Shop VAT number for non-Union scheme", + Example = "EU123456789", + ValidationExpression = new Regex("^EU[0-9]{9}$") + }, + new() + { + Country = "FI", + Code = "eu_vat", + Description = "European VAT number (Finland)", + Example = "FI12345678", + ValidationExpression = new Regex("^FI[0-9]{8}$") + }, + new() + { + Country = "FR", + Code = "eu_vat", + Description = "European VAT number (France)", + Example = "FR12345678901", + ValidationExpression = new Regex("^FR[0-9A-Z]{2}[0-9]{9}$") + }, + new() + { + Country = "GE", + Code = "ge_vat", + Description = "Georgian VAT", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "DE", + Code = "de_stn", + Description = "German Tax Number (Steuernummer)", + Example = "1234567890", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "DE", + Code = "eu_vat", + Description = "European VAT number (Germany)", + Example = "DE123456789", + ValidationExpression = new Regex("^DE[0-9]{9}$") + }, + new() + { + Country = "GR", + Code = "eu_vat", + Description = "European VAT number (Greece)", + Example = "EL123456789", + ValidationExpression = new Regex("^EL[0-9]{9}$") + }, + new() + { + Country = "HK", + Code = "hk_br", + Description = "Hong Kong BR number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "HU", + Code = "eu_vat", + Description = "European VAT number (Hungaria)", + Example = "HU12345678", + ValidationExpression = new Regex("^HU[0-9]{8}$") + }, + new() + { + Country = "HU", + Code = "hu_tin", + Description = "Hungary tax number (adószám)", + Example = "12345678-1-23", + ValidationExpression = new Regex("^[0-9]{8}-?[0-9]-?[0-9]{2}$") + }, + new() + { + Country = "IS", + Code = "is_vat", + Description = "Icelandic VAT", + Example = "123456", + ValidationExpression = new Regex("^[0-9]{6}$") + }, + new() + { + Country = "IN", + Code = "in_gst", + Description = "Indian GST number", + Example = "12ABCDE3456FGZH", + ValidationExpression = new Regex("^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$") + }, + new() + { + Country = "ID", + Code = "id_npwp", + Description = "Indonesian NPWP number", + Example = "012.345.678.9-012.345", + ValidationExpression = new Regex("^[0-9]{3}.?[0-9]{3}.?[0-9]{3}.?[0-9]{1}-?[0-9]{3}.?[0-9]{3}$") + }, + new() + { + Country = "IE", + Code = "eu_vat", + Description = "European VAT number (Ireland)", + Example = "IE1234567AB", + ValidationExpression = new Regex("^IE[0-9]{7}[A-Z]{1,2}$") + }, + new() + { + Country = "IL", + Code = "il_vat", + Description = "Israel VAT", + Example = "000012345", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "IT", + Code = "eu_vat", + Description = "European VAT number (Italy)", + Example = "IT12345678912", + ValidationExpression = new Regex("^IT[0-9]{11}$") + }, + new() + { + Country = "JP", + Code = "jp_cn", + Description = "Japanese Corporate Number (*Hōjin Bangō*)", + Example = "1234567891234", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "JP", + Code = "jp_rn", + Description = + "Japanese Registered Foreign Businesses' Registration Number (*Tōroku Kokugai Jigyōsha no Tōroku Bangō*)", + Example = "12345", + ValidationExpression = new Regex("^[0-9]{5}$") + }, + new() + { + Country = "JP", + Code = "jp_trn", + Description = "Japanese Tax Registration Number (*Tōroku Bangō*)", + Example = "T1234567891234", + ValidationExpression = new Regex("^T[0-9]{13}$") + }, + new() + { + Country = "KZ", + Code = "kz_bin", + Description = "Kazakhstani Business Identification Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "KE", + Code = "ke_pin", + Description = "Kenya Revenue Authority Personal Identification Number", + Example = "P000111111A", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{9}[A-Z]{1}$") + }, + new() + { + Country = "LV", + Code = "eu_vat", + Description = "European VAT number", + Example = "LV12345678912", + ValidationExpression = new Regex("^LV[0-9]{11}$") + }, + new() + { + Country = "LI", + Code = "li_uid", + Description = "Liechtensteinian UID number", + Example = "CHE123456789", + ValidationExpression = new Regex("^CHE[0-9]{9}$") + }, + new() + { + Country = "LI", + Code = "li_vat", + Description = "Liechtensteinian VAT number", + Example = "12345", + ValidationExpression = new Regex("^[0-9]{5}$") + }, + new() + { + Country = "LT", + Code = "eu_vat", + Description = "European VAT number (Lithuania)", + Example = "LT123456789123", + ValidationExpression = new Regex("^LT[0-9]{9,12}$") + }, + new() + { + Country = "LU", + Code = "eu_vat", + Description = "European VAT number (Luxembourg)", + Example = "LU12345678", + ValidationExpression = new Regex("^LU[0-9]{8}$") + }, + new() + { + Country = "MY", + Code = "my_frp", + Description = "Malaysian FRP number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "MY", + Code = "my_itn", + Description = "Malaysian ITN", + Example = "C 1234567890", + ValidationExpression = new Regex("^[A-Z]{1} ?[0-9]{10}$") + }, + new() + { + Country = "MY", + Code = "my_sst", + Description = "Malaysian SST number", + Example = "A12-3456-78912345", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{2}-?[0-9]{4}-?[0-9]{8}$") + }, + new() + { + Country = "MT", + Code = "eu_vat", + Description = "European VAT number (Malta)", + Example = "MT12345678", + ValidationExpression = new Regex("^MT[0-9]{8}$") + }, + new() + { + Country = "MX", + Code = "mx_rfc", + Description = "Mexican RFC number", + Example = "ABC010203AB9", + ValidationExpression = new Regex("^[A-Z]{3}[0-9]{6}[A-Z0-9]{3}$") + }, + new() + { + Country = "MD", + Code = "md_vat", + Description = "Moldova VAT Number", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "MA", + Code = "ma_vat", + Description = "Morocco VAT Number", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "NL", + Code = "eu_vat", + Description = "European VAT number (Netherlands)", + Example = "NL123456789B12", + ValidationExpression = new Regex("^NL[0-9]{9}B[0-9]{2}$") + }, + new() + { + Country = "NZ", + Code = "nz_gst", + Description = "New Zealand GST number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "NG", + Code = "ng_tin", + Description = "Nigerian TIN Number", + Example = "12345678-0001", + ValidationExpression = new Regex("^[0-9]{8}-[0-9]{4}$") + }, + new() + { + Country = "NO", + Code = "no_vat", + Description = "Norwegian VAT number", + Example = "123456789MVA", + ValidationExpression = new Regex("^[0-9]{9}MVA$") + }, + new() + { + Country = "NO", + Code = "no_voec", + Description = "Norwegian VAT on e-commerce number", + Example = "1234567", + ValidationExpression = new Regex("^[0-9]{7}$") + }, + new() + { + Country = "OM", + Code = "om_vat", + Description = "Omani VAT Number", + Example = "OM1234567890", + ValidationExpression = new Regex("^OM[0-9]{10}$") + }, + new() + { + Country = "PE", + Code = "pe_ruc", + Description = "Peruvian RUC number", + Example = "12345678901", + ValidationExpression = new Regex("^[0-9]{11}$") + }, + new() + { + Country = "PH", + Code = "ph_tin", + Description = "Philippines Tax Identification Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "PL", + Code = "eu_vat", + Description = "European VAT number (Poland)", + Example = "PL1234567890", + ValidationExpression = new Regex("^PL[0-9]{10}$") + }, + new() + { + Country = "PT", + Code = "eu_vat", + Description = "European VAT number (Portugal)", + Example = "PT123456789", + ValidationExpression = new Regex("^PT[0-9]{9}$") + }, + new() + { + Country = "RO", + Code = "eu_vat", + Description = "European VAT number (Romania)", + Example = "RO1234567891", + ValidationExpression = new Regex("^RO[0-9]{2,10}$") + }, + new() + { + Country = "RO", + Code = "ro_tin", + Description = "Romanian tax ID number", + Example = "1234567890123", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "RU", + Code = "ru_inn", + Description = "Russian INN", + Example = "1234567891", + ValidationExpression = new Regex("^[0-9]{10,12}$") + }, + new() + { + Country = "RU", + Code = "ru_kpp", + Description = "Russian KPP", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "SA", + Code = "sa_vat", + Description = "Saudi Arabia VAT", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "RS", + Code = "rs_pib", + Description = "Serbian PIB number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "SG", + Code = "sg_gst", + Description = "Singaporean GST", + Example = "M12345678X", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "SG", + Code = "sg_uen", + Description = "Singaporean UEN", + Example = "123456789F", + ValidationExpression = new Regex("^[0-9]{9}[A-Z]{1}$") + }, + new() + { + Country = "SK", + Code = "eu_vat", + Description = "European VAT number (Slovakia)", + Example = "SK1234567891", + ValidationExpression = new Regex("^SK[0-9]{10}$") + }, + new() + { + Country = "SI", + Code = "eu_vat", + Description = "European VAT number (Slovenia)", + Example = "SI12345678", + ValidationExpression = new Regex("^SI[0-9]{8}$") + }, + new() + { + Country = "SI", + Code = "si_tin", + Description = "Slovenia tax number (davčna številka)", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "ZA", + Code = "za_vat", + Description = "South African VAT number", + Example = "4123456789", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "KR", + Code = "kr_brn", + Description = "Korean BRN", + Example = "123-45-67890", + ValidationExpression = new Regex("^[0-9]{3}-?[0-9]{2}-?[0-9]{5}$") + }, + new() + { + Country = "ES", + Code = "es_cif", + Description = "Spanish NIF/CIF number", + Example = "A12345678", + ValidationExpression = new Regex("^[A-Z]{1}[0-9]{8}$") + }, + new() + { + Country = "ES", + Code = "eu_vat", + Description = "European VAT number (Spain)", + Example = "ESA1234567Z", + ValidationExpression = new Regex("^ES[A-Z]{1}[0-9]{7}[A-Z]{1}$") + }, + new() + { + Country = "SE", + Code = "eu_vat", + Description = "European VAT number (Sweden)", + Example = "SE123456789123", + ValidationExpression = new Regex("^SE[0-9]{12}$") + }, + new() + { + Country = "CH", + Code = "ch_uid", + Description = "Switzerland UID number", + Example = "CHE-123.456.789 HR", + ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?HR$") + }, + new() + { + Country = "CH", + Code = "ch_vat", + Description = "Switzerland VAT number", + Example = "CHE-123.456.789 MWST", + ValidationExpression = new Regex("^CHE-?[0-9]{3}.?[0-9]{3}.?[0-9]{3} ?MWST$") + }, + new() + { + Country = "TW", + Code = "tw_vat", + Description = "Taiwanese VAT", + Example = "12345678", + ValidationExpression = new Regex("^[0-9]{8}$") + }, + new() + { + Country = "TZ", + Code = "tz_vat", + Description = "Tanzania VAT Number", + Example = "12345678A", + ValidationExpression = new Regex("^[0-9]{8}[A-Z]{1}$") + }, + new() + { + Country = "TH", + Code = "th_vat", + Description = "Thai VAT", + Example = "1234567891234", + ValidationExpression = new Regex("^[0-9]{13}$") + }, + new() + { + Country = "TR", + Code = "tr_tin", + Description = "Turkish TIN Number", + Example = "0123456789", + ValidationExpression = new Regex("^[0-9]{10}$") + }, + new() + { + Country = "UA", + Code = "ua_vat", + Description = "Ukrainian VAT", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "AE", + Code = "ae_trn", + Description = "United Arab Emirates TRN", + Example = "123456789012345", + ValidationExpression = new Regex("^[0-9]{15}$") + }, + new() + { + Country = "GB", + Code = "eu_vat", + Description = "Northern Ireland VAT number", + Example = "XI123456789", + ValidationExpression = new Regex("^XI[0-9]{9}$") + }, + new() + { + Country = "GB", + Code = "gb_vat", + Description = "United Kingdom VAT number", + Example = "GB123456789", + ValidationExpression = new Regex("^GB[0-9]{9}$") + }, + new() + { + Country = "US", + Code = "us_ein", + Description = "United States EIN", + Example = "12-3456789", + ValidationExpression = new Regex("^[0-9]{2}-?[0-9]{7}$") + }, + new() + { + Country = "UY", + Code = "uy_ruc", + Description = "Uruguayan RUC number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "UZ", + Code = "uz_tin", + Description = "Uzbekistan TIN Number", + Example = "123456789", + ValidationExpression = new Regex("^[0-9]{9}$") + }, + new() + { + Country = "UZ", + Code = "uz_vat", + Description = "Uzbekistan VAT Number", + Example = "123456789012", + ValidationExpression = new Regex("^[0-9]{12}$") + }, + new() + { + Country = "VE", + Code = "ve_rif", + Description = "Venezuelan RIF number", + Example = "A-12345678-9", + ValidationExpression = new Regex("^[A-Z]{1}-?[0-9]{8}-?[0-9]{1}$") + }, + new() + { + Country = "VN", + Code = "vn_tin", + Description = "Vietnamese tax ID number", + Example = "1234567890", + ValidationExpression = new Regex("^[0-9]{10}$") + } + ]; + + /// + /// Retrieves the Stripe tax code for a given country and tax ID. + /// + /// + /// + /// + /// Returns the Stripe tax code if the tax ID is valid for the country. + /// Returns null if the tax ID is invalid or the country is not supported. + /// + public string? GetStripeTaxCode(string country, string taxId) + { + foreach (var taxIdType in _taxIdTypes.Where(x => x.Country == country)) + { + if (taxIdType.ValidationExpression.IsMatch(taxId)) + { + return taxIdType.Code; + } + } + + return null; + } + + public IEnumerable GetTaxIdTypes() => _taxIdTypes; +} diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index b8bc1887b..69651533a 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -82,6 +82,7 @@ public static class Utilities customer.Address.Country, customer.Address.PostalCode, customer.TaxIds?.FirstOrDefault()?.Value, + customer.TaxIds?.FirstOrDefault()?.Type, customer.Address.Line1, customer.Address.Line2, customer.Address.City, diff --git a/src/Core/Models/Business/TaxInfo.cs b/src/Core/Models/Business/TaxInfo.cs index 4424576ec..c036a90d3 100644 --- a/src/Core/Models/Business/TaxInfo.cs +++ b/src/Core/Models/Business/TaxInfo.cs @@ -2,18 +2,10 @@ public class TaxInfo { - private string _taxIdNumber = null; - private string _taxIdType = null; + public string? TaxIdNumber { get; set; } + public string? TaxIdType { get; set; } + - public string TaxIdNumber - { - get => _taxIdNumber; - set - { - _taxIdNumber = value; - _taxIdType = null; - } - } public string StripeTaxRateId { get; set; } public string BillingAddressLine1 { get; set; } public string BillingAddressLine2 { get; set; } @@ -21,201 +13,6 @@ public class TaxInfo public string BillingAddressState { get; set; } public string BillingAddressPostalCode { get; set; } public string BillingAddressCountry { get; set; } = "US"; - public string TaxIdType - { - get - { - if (string.IsNullOrWhiteSpace(BillingAddressCountry) || - string.IsNullOrWhiteSpace(TaxIdNumber)) - { - return null; - } - if (!string.IsNullOrWhiteSpace(_taxIdType)) - { - return _taxIdType; - } - - switch (BillingAddressCountry.ToUpper()) - { - case "AD": - _taxIdType = "ad_nrt"; - break; - case "AE": - _taxIdType = "ae_trn"; - break; - case "AR": - _taxIdType = "ar_cuit"; - break; - case "AU": - _taxIdType = "au_abn"; - break; - case "BO": - _taxIdType = "bo_tin"; - break; - case "BR": - _taxIdType = "br_cnpj"; - break; - case "CA": - // May break for those in Québec given the assumption of QST - if (BillingAddressState?.Contains("bec") ?? false) - { - _taxIdType = "ca_qst"; - break; - } - _taxIdType = "ca_bn"; - break; - case "CH": - _taxIdType = "ch_vat"; - break; - case "CL": - _taxIdType = "cl_tin"; - break; - case "CN": - _taxIdType = "cn_tin"; - break; - case "CO": - _taxIdType = "co_nit"; - break; - case "CR": - _taxIdType = "cr_tin"; - break; - case "DO": - _taxIdType = "do_rcn"; - break; - case "EC": - _taxIdType = "ec_ruc"; - break; - case "EG": - _taxIdType = "eg_tin"; - break; - case "GE": - _taxIdType = "ge_vat"; - break; - case "ID": - _taxIdType = "id_npwp"; - break; - case "IL": - _taxIdType = "il_vat"; - break; - case "IS": - _taxIdType = "is_vat"; - break; - case "KE": - _taxIdType = "ke_pin"; - break; - case "AT": - case "BE": - case "BG": - case "CY": - case "CZ": - case "DE": - case "DK": - case "EE": - case "ES": - case "FI": - case "FR": - case "GB": - case "GR": - case "HR": - case "HU": - case "IE": - case "IT": - case "LT": - case "LU": - case "LV": - case "MT": - case "NL": - case "PL": - case "PT": - case "RO": - case "SE": - case "SI": - case "SK": - _taxIdType = "eu_vat"; - break; - case "HK": - _taxIdType = "hk_br"; - break; - case "IN": - _taxIdType = "in_gst"; - break; - case "JP": - _taxIdType = "jp_cn"; - break; - case "KR": - _taxIdType = "kr_brn"; - break; - case "LI": - _taxIdType = "li_uid"; - break; - case "MX": - _taxIdType = "mx_rfc"; - break; - case "MY": - _taxIdType = "my_sst"; - break; - case "NO": - _taxIdType = "no_vat"; - break; - case "NZ": - _taxIdType = "nz_gst"; - break; - case "PE": - _taxIdType = "pe_ruc"; - break; - case "PH": - _taxIdType = "ph_tin"; - break; - case "RS": - _taxIdType = "rs_pib"; - break; - case "RU": - _taxIdType = "ru_inn"; - break; - case "SA": - _taxIdType = "sa_vat"; - break; - case "SG": - _taxIdType = "sg_gst"; - break; - case "SV": - _taxIdType = "sv_nit"; - break; - case "TH": - _taxIdType = "th_vat"; - break; - case "TR": - _taxIdType = "tr_tin"; - break; - case "TW": - _taxIdType = "tw_vat"; - break; - case "UA": - _taxIdType = "ua_vat"; - break; - case "US": - _taxIdType = "us_ein"; - break; - case "UY": - _taxIdType = "uy_ruc"; - break; - case "VE": - _taxIdType = "ve_rif"; - break; - case "VN": - _taxIdType = "vn_tin"; - break; - case "ZA": - _taxIdType = "za_vat"; - break; - default: - _taxIdType = null; - break; - } - - return _taxIdType; - } - } public bool HasTaxId { diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 7eb2b402b..4b7e9bd42 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1694,14 +1694,42 @@ public class StripePaymentService : IPaymentService { await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id); } - if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) && - !string.IsNullOrWhiteSpace(taxInfo.TaxIdType)) + + if (taxInfo.TaxIdType == null) { - await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions + _logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", + taxInfo.BillingAddressCountry, + taxInfo.TaxIdNumber); + throw new BadRequestException("billingTaxIdTypeInferenceError"); + } + + if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) + { + try { - Type = taxInfo.TaxIdType, - Value = taxInfo.TaxIdNumber, - }); + await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions + { + Type = taxInfo.TaxIdType, + Value = taxInfo.TaxIdNumber, + }); + } + catch (StripeException e) + { + switch (e.StripeError.Code) + { + case "tax_id_invalid": + _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"); + } + } } } } diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 2816e5c87..78fcd0d99 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,6 +1,5 @@ using System.Collections.Immutable; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -172,294 +171,4 @@ public static class StaticStore p.PasswordManager.StripeStoragePlanId == stripePlanId || (p.SecretsManager?.StripeServiceAccountPlanId == stripePlanId)); } - - public static readonly IEnumerable SupportedTaxIdTypes = - [ - new() { Country = "AD", Code = "ad_nrt", Description = "Andorran NRT number", Example = "A-123456-Z" }, - new() - { - Country = "AR", - Code = "ar_cuit", - Description = "Argentinian tax ID number", - Example = "12-3456789-01" - }, - new() - { - Country = "AU", - Code = "au_abn", - Description = "Australian Business Number (AU ABN)", - Example = "12345678912" - }, - new() - { - Country = "AU", - Code = "au_arn", - Description = "Australian Taxation Office Reference Number", - Example = "123456789123" - }, - new() { Country = "AT", Code = "eu_vat", Description = "European VAT number", Example = "ATU12345678" }, - new() { Country = "BH", Code = "bh_vat", Description = "Bahraini VAT Number", Example = "123456789012345" }, - new() { Country = "BY", Code = "by_tin", Description = "Belarus TIN Number", Example = "123456789" }, - new() { Country = "BE", Code = "eu_vat", Description = "European VAT number", Example = "BE0123456789" }, - new() { Country = "BO", Code = "bo_tin", Description = "Bolivian tax ID", Example = "123456789" }, - new() - { - Country = "BR", - Code = "br_cnpj", - Description = "Brazilian CNPJ number", - Example = "01.234.456/5432-10" - }, - new() { Country = "BR", Code = "br_cpf", Description = "Brazilian CPF number", Example = "123.456.789-87" }, - new() - { - Country = "BG", - Code = "bg_uic", - Description = "Bulgaria Unified Identification Code", - Example = "123456789" - }, - new() { Country = "BG", Code = "eu_vat", Description = "European VAT number", Example = "BG0123456789" }, - new() { Country = "CA", Code = "ca_bn", Description = "Canadian BN", Example = "123456789" }, - new() - { - Country = "CA", - Code = "ca_gst_hst", - Description = "Canadian GST/HST number", - Example = "123456789RT0002" - }, - new() - { - Country = "CA", - Code = "ca_pst_bc", - Description = "Canadian PST number (British Columbia)", - Example = "PST-1234-5678" - }, - new() - { - Country = "CA", - Code = "ca_pst_mb", - Description = "Canadian PST number (Manitoba)", - Example = "123456-7" - }, - new() - { - Country = "CA", - Code = "ca_pst_sk", - Description = "Canadian PST number (Saskatchewan)", - Example = "1234567" - }, - new() - { - Country = "CA", - Code = "ca_qst", - Description = "Canadian QST number (Québec)", - Example = "1234567890TQ1234" - }, - new() { Country = "CL", Code = "cl_tin", Description = "Chilean TIN", Example = "12.345.678-K" }, - new() { Country = "CN", Code = "cn_tin", Description = "Chinese tax ID", Example = "123456789012345678" }, - new() { Country = "CO", Code = "co_nit", Description = "Colombian NIT number", Example = "123.456.789-0" }, - new() { Country = "CR", Code = "cr_tin", Description = "Costa Rican tax ID", Example = "1-234-567890" }, - new() { Country = "HR", Code = "eu_vat", Description = "European VAT number", Example = "HR12345678912" }, - new() - { - Country = "HR", - Code = "hr_oib", - Description = "Croatian Personal Identification Number", - Example = "12345678901" - }, - new() { Country = "CY", Code = "eu_vat", Description = "European VAT number", Example = "CY12345678Z" }, - new() { Country = "CZ", Code = "eu_vat", Description = "European VAT number", Example = "CZ1234567890" }, - new() { Country = "DK", Code = "eu_vat", Description = "European VAT number", Example = "DK12345678" }, - new() { Country = "DO", Code = "do_rcn", Description = "Dominican RCN number", Example = "123-4567890-1" }, - new() { Country = "EC", Code = "ec_ruc", Description = "Ecuadorian RUC number", Example = "1234567890001" }, - new() - { - Country = "EG", - Code = "eg_tin", - Description = "Egyptian Tax Identification Number", - Example = "123456789" - }, - - new() - { - Country = "SV", - Code = "sv_nit", - Description = "El Salvadorian NIT number", - Example = "1234-567890-123-4" - }, - - new() { Country = "EE", Code = "eu_vat", Description = "European VAT number", Example = "EE123456789" }, - - new() - { - Country = "EU", - Code = "eu_oss_vat", - Description = "European One Stop Shop VAT number for non-Union scheme", - Example = "EU123456789" - }, - new() { Country = "FI", Code = "eu_vat", Description = "European VAT number", Example = "FI12345678" }, - new() { Country = "FR", Code = "eu_vat", Description = "European VAT number", Example = "FRAB123456789" }, - new() { Country = "GE", Code = "ge_vat", Description = "Georgian VAT", Example = "123456789" }, - new() - { - Country = "DE", - Code = "de_stn", - Description = "German Tax Number (Steuernummer)", - Example = "1234567890" - }, - new() { Country = "DE", Code = "eu_vat", Description = "European VAT number", Example = "DE123456789" }, - new() { Country = "GR", Code = "eu_vat", Description = "European VAT number", Example = "EL123456789" }, - new() { Country = "HK", Code = "hk_br", Description = "Hong Kong BR number", Example = "12345678" }, - new() { Country = "HU", Code = "eu_vat", Description = "European VAT number", Example = "HU12345678" }, - new() - { - Country = "HU", - Code = "hu_tin", - Description = "Hungary tax number (adószám)", - Example = "12345678-1-23" - }, - new() { Country = "IS", Code = "is_vat", Description = "Icelandic VAT", Example = "123456" }, - new() { Country = "IN", Code = "in_gst", Description = "Indian GST number", Example = "12ABCDE3456FGZH" }, - new() - { - Country = "ID", - Code = "id_npwp", - Description = "Indonesian NPWP number", - Example = "012.345.678.9-012.345" - }, - new() { Country = "IE", Code = "eu_vat", Description = "European VAT number", Example = "IE1234567AB" }, - new() { Country = "IL", Code = "il_vat", Description = "Israel VAT", Example = "000012345" }, - new() { Country = "IT", Code = "eu_vat", Description = "European VAT number", Example = "IT12345678912" }, - new() - { - Country = "JP", - Code = "jp_cn", - Description = "Japanese Corporate Number (*Hōjin Bangō*)", - Example = "1234567891234" - }, - new() - { - Country = "JP", - Code = "jp_rn", - Description = - "Japanese Registered Foreign Businesses' Registration Number (*Tōroku Kokugai Jigyōsha no Tōroku Bangō*)", - Example = "12345" - }, - new() - { - Country = "JP", - Code = "jp_trn", - Description = "Japanese Tax Registration Number (*Tōroku Bangō*)", - Example = "T1234567891234" - }, - new() - { - Country = "KZ", - Code = "kz_bin", - Description = "Kazakhstani Business Identification Number", - Example = "123456789012" - }, - new() - { - Country = "KE", - Code = "ke_pin", - Description = "Kenya Revenue Authority Personal Identification Number", - Example = "P000111111A" - }, - new() { Country = "LV", Code = "eu_vat", Description = "European VAT number", Example = "LV12345678912" }, - new() - { - Country = "LI", - Code = "li_uid", - Description = "Liechtensteinian UID number", - Example = "CHE123456789" - }, - new() { Country = "LI", Code = "li_vat", Description = "Liechtensteinian VAT number", Example = "12345" }, - new() { Country = "LT", Code = "eu_vat", Description = "European VAT number", Example = "LT123456789123" }, - new() { Country = "LU", Code = "eu_vat", Description = "European VAT number", Example = "LU12345678" }, - new() { Country = "MY", Code = "my_frp", Description = "Malaysian FRP number", Example = "12345678" }, - new() { Country = "MY", Code = "my_itn", Description = "Malaysian ITN", Example = "C 1234567890" }, - new() { Country = "MY", Code = "my_sst", Description = "Malaysian SST number", Example = "A12-3456-78912345" }, - new() { Country = "MT", Code = "eu_vat", Description = "European VAT number", Example = "MT12345678" }, - new() { Country = "MX", Code = "mx_rfc", Description = "Mexican RFC number", Example = "ABC010203AB9" }, - new() { Country = "MD", Code = "md_vat", Description = "Moldova VAT Number", Example = "1234567" }, - new() { Country = "MA", Code = "ma_vat", Description = "Morocco VAT Number", Example = "12345678" }, - new() { Country = "NL", Code = "eu_vat", Description = "European VAT number", Example = "NL123456789B12" }, - new() { Country = "NZ", Code = "nz_gst", Description = "New Zealand GST number", Example = "123456789" }, - new() { Country = "NG", Code = "ng_tin", Description = "Nigerian TIN Number", Example = "12345678-0001" }, - new() { Country = "NO", Code = "no_vat", Description = "Norwegian VAT number", Example = "123456789MVA" }, - new() - { - Country = "NO", - Code = "no_voec", - Description = "Norwegian VAT on e-commerce number", - Example = "1234567" - }, - new() { Country = "OM", Code = "om_vat", Description = "Omani VAT Number", Example = "OM1234567890" }, - new() { Country = "PE", Code = "pe_ruc", Description = "Peruvian RUC number", Example = "12345678901" }, - new() - { - Country = "PH", - Code = "ph_tin", - Description = "Philippines Tax Identification Number", - Example = "123456789012" - }, - new() { Country = "PL", Code = "eu_vat", Description = "European VAT number", Example = "PL1234567890" }, - new() { Country = "PT", Code = "eu_vat", Description = "European VAT number", Example = "PT123456789" }, - new() { Country = "RO", Code = "eu_vat", Description = "European VAT number", Example = "RO1234567891" }, - new() { Country = "RO", Code = "ro_tin", Description = "Romanian tax ID number", Example = "1234567890123" }, - new() { Country = "RU", Code = "ru_inn", Description = "Russian INN", Example = "1234567891" }, - new() { Country = "RU", Code = "ru_kpp", Description = "Russian KPP", Example = "123456789" }, - new() { Country = "SA", Code = "sa_vat", Description = "Saudi Arabia VAT", Example = "123456789012345" }, - new() { Country = "RS", Code = "rs_pib", Description = "Serbian PIB number", Example = "123456789" }, - new() { Country = "SG", Code = "sg_gst", Description = "Singaporean GST", Example = "M12345678X" }, - new() { Country = "SG", Code = "sg_uen", Description = "Singaporean UEN", Example = "123456789F" }, - new() { Country = "SK", Code = "eu_vat", Description = "European VAT number", Example = "SK1234567891" }, - new() { Country = "SI", Code = "eu_vat", Description = "European VAT number", Example = "SI12345678" }, - new() - { - Country = "SI", - Code = "si_tin", - Description = "Slovenia tax number (davčna številka)", - Example = "12345678" - }, - new() { Country = "ZA", Code = "za_vat", Description = "South African VAT number", Example = "4123456789" }, - new() { Country = "KR", Code = "kr_brn", Description = "Korean BRN", Example = "123-45-67890" }, - new() { Country = "ES", Code = "es_cif", Description = "Spanish NIF/CIF number", Example = "A12345678" }, - new() { Country = "ES", Code = "eu_vat", Description = "European VAT number", Example = "ESA1234567Z" }, - new() { Country = "SE", Code = "eu_vat", Description = "European VAT number", Example = "SE123456789123" }, - new() - { - Country = "CH", - Code = "ch_uid", - Description = "Switzerland UID number", - Example = "CHE-123.456.789 HR" - }, - new() - { - Country = "CH", - Code = "ch_vat", - Description = "Switzerland VAT number", - Example = "CHE-123.456.789 MWST" - }, - new() { Country = "TW", Code = "tw_vat", Description = "Taiwanese VAT", Example = "12345678" }, - new() { Country = "TZ", Code = "tz_vat", Description = "Tanzania VAT Number", Example = "12345678A" }, - new() { Country = "TH", Code = "th_vat", Description = "Thai VAT", Example = "1234567891234" }, - new() { Country = "TR", Code = "tr_tin", Description = "Turkish TIN Number", Example = "0123456789" }, - new() { Country = "UA", Code = "ua_vat", Description = "Ukrainian VAT", Example = "123456789" }, - new() - { - Country = "AE", - Code = "ae_trn", - Description = "United Arab Emirates TRN", - Example = "123456789012345" - }, - new() { Country = "GB", Code = "eu_vat", Description = "Northern Ireland VAT number", Example = "XI123456789" }, - new() { Country = "GB", Code = "gb_vat", Description = "United Kingdom VAT number", Example = "GB123456789" }, - new() { Country = "US", Code = "us_ein", Description = "United States EIN", Example = "12-3456789" }, - new() { Country = "UY", Code = "uy_ruc", Description = "Uruguayan RUC number", Example = "123456789012" }, - new() { Country = "UZ", Code = "uz_tin", Description = "Uzbekistan TIN Number", Example = "123456789" }, - new() { Country = "UZ", Code = "uz_vat", Description = "Uzbekistan VAT Number", Example = "123456789012" }, - new() { Country = "VE", Code = "ve_rif", Description = "Venezuelan RIF number", Example = "A-12345678-9" }, - new() { Country = "VN", Code = "vn_tin", Description = "Vietnamese tax ID number", Example = "1234567890" } - ]; } diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 652c22764..1c882cbd2 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -1545,7 +1545,7 @@ public class SubscriberServiceTests { var stripeAdapter = sutProvider.GetDependency(); - var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1" }] } }; + var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } }; stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is( options => options.Expand.Contains("tax_ids"))).Returns(customer); @@ -1554,6 +1554,7 @@ public class SubscriberServiceTests "US", "12345", "123456789", + "us_ein", "123 Example St.", null, "Example Town",