Skip to content

Commit

Permalink
[PM-13999] Show estimated tax for taxable countries (#5077)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonashendrickx authored Dec 4, 2024
1 parent 44b6879 commit 94fdfa4
Show file tree
Hide file tree
Showing 30 changed files with 1,791 additions and 529 deletions.
151 changes: 151 additions & 0 deletions bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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<TaxService> sutProvider)
{
var result = sutProvider.Sut.GetStripeTaxCode(country, taxId);

Assert.Equal(expected, result);
}
}
15 changes: 15 additions & 0 deletions src/Api/Billing/Controllers/AccountsBillingController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Utilities;
Expand Down Expand Up @@ -77,4 +78,18 @@ public async Task<IResult> GetTransactionsAsync([FromQuery] DateTime? startAfter

return TypedResults.Ok(transactions);
}

[HttpPost("preview-invoice")]
public async Task<IResult> PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}

var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId);

return TypedResults.Ok(invoice);
}
}
42 changes: 42 additions & 0 deletions src/Api/Billing/Controllers/InvoicesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models.Api.Requests.Organizations;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Billing.Controllers;

[Route("invoices")]
[Authorize("Application")]
public class InvoicesController : BaseBillingController
{
[HttpPost("preview-organization")]
public async Task<IResult> PreviewInvoiceAsync(
[FromBody] PreviewOrganizationInvoiceRequestBody model,
[FromServices] ICurrentContext currentContext,
[FromServices] IOrganizationRepository organizationRepository,
[FromServices] IPaymentService paymentService)
{
Organization organization = null;
if (model.OrganizationId != default)
{
if (!await currentContext.EditPaymentMethods(model.OrganizationId))
{
return Error.Unauthorized();
}

organization = await organizationRepository.GetByIdAsync(model.OrganizationId);
if (organization == null)
{
return Error.NotFound();
}
}

var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId,
organization?.GatewaySubscriptionId);

return TypedResults.Ok(invoice);
}
}
1 change: 1 addition & 0 deletions src/Api/Billing/Controllers/ProviderBillingController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public async Task<IResult> UpdateTaxInformationAsync(
requestBody.Country,
requestBody.PostalCode,
requestBody.TaxId,
requestBody.TaxIdType,
requestBody.Line1,
requestBody.Line2,
requestBody.City,
Expand Down
14 changes: 13 additions & 1 deletion src/Api/Billing/Controllers/StripeController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -46,4 +47,15 @@ public async Task<Ok<string>> CreateSetupIntentForCardAsync()

return TypedResults.Ok(setupIntent.ClientSecret);
}

[HttpGet]
[Route("~/tax/is-country-supported")]
public IResult IsCountrySupported(
[FromQuery] string country,
[FromServices] ITaxService taxService)
{
var isSupported = taxService.IsSupported(country);

return TypedResults.Ok(isSupported);
}
}
2 changes: 2 additions & 0 deletions src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -19,6 +20,7 @@ public class TaxInformationRequestBody
Country,
PostalCode,
TaxId,
TaxIdType,
Line1,
Line2,
City,
Expand Down
33 changes: 33 additions & 0 deletions src/Core/Billing/Extensions/CurrencyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Bit.Core.Billing.Extensions;

public static class CurrencyExtensions
{
/// <summary>
/// Converts a currency amount in major units to minor units.
/// </summary>
/// <example>123.99 USD returns 12399 in minor units.</example>
public static long ToMinor(this decimal amount)
{
return Convert.ToInt64(amount * 100);
}

/// <summary>
/// Converts a currency amount in minor units to major units.
/// </summary>
/// <param name="amount"></param>
/// <example>12399 in minor units returns 123.99 USD.</example>
public static decimal? ToMajor(this long? amount)
{
return amount?.ToMajor();
}

/// <summary>
/// Converts a currency amount in minor units to major units.
/// </summary>
/// <param name="amount"></param>
/// <example>12399 in minor units returns 123.99 USD.</example>
public static decimal ToMajor(this long amount)
{
return Convert.ToDecimal(amount) / 100;
}
}
1 change: 1 addition & 0 deletions src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static class ServiceCollectionExtensions
{
public static void AddBillingOperations(this IServiceCollection services)
{
services.AddSingleton<ITaxService, TaxService>();
services.AddTransient<IOrganizationBillingService, OrganizationBillingService>();
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.Billing.Models.Api.Requests.Accounts;

public class PreviewIndividualInvoiceRequestBody
{
[Required]
public PasswordManagerRequestModel PasswordManager { get; set; }

[Required]
public TaxInformationRequestModel TaxInformation { get; set; }
}

public class PasswordManagerRequestModel
{
[Range(0, int.MaxValue)]
public int AdditionalStorage { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Enums;

namespace Bit.Core.Billing.Models.Api.Requests.Organizations;

public class PreviewOrganizationInvoiceRequestBody
{
public Guid OrganizationId { get; set; }

[Required]
public PasswordManagerRequestModel PasswordManager { get; set; }

public SecretsManagerRequestModel SecretsManager { get; set; }

[Required]
public TaxInformationRequestModel TaxInformation { get; set; }
}

public class PasswordManagerRequestModel
{
public PlanType Plan { get; set; }

[Range(0, int.MaxValue)]
public int Seats { get; set; }

[Range(0, int.MaxValue)]
public int AdditionalStorage { get; set; }
}

public class SecretsManagerRequestModel
{
[Range(0, int.MaxValue)]
public int Seats { get; set; }

[Range(0, int.MaxValue)]
public int AdditionalMachineAccounts { get; set; }
}
14 changes: 14 additions & 0 deletions src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.Billing.Models.Api.Requests;

public class TaxInformationRequestModel
{
[Length(2, 2), Required]
public string Country { get; set; }

[Required]
public string PostalCode { get; set; }

public string TaxId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Bit.Core.Billing.Models.Api.Responses;

public record PreviewInvoiceResponseModel(
decimal EffectiveTaxRate,
decimal TaxableBaseAmount,
decimal TaxAmount,
decimal TotalAmount);
7 changes: 7 additions & 0 deletions src/Core/Billing/Models/PreviewInvoiceInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Bit.Core.Billing.Models;

public record PreviewInvoiceInfo(
decimal EffectiveTaxRate,
decimal TaxableBaseAmount,
decimal TaxAmount,
decimal TotalAmount);
Loading

0 comments on commit 94fdfa4

Please sign in to comment.