mirror of
https://github.com/bitwarden/server.git
synced 2025-07-12 05:13:58 -05:00
[PM-20087] [PM-21104] Preview tax amount for organization trial initiation (#5787)
* [NO LOGIC] [PM-21104] Organize Core.Billing tax code * Add PreviewTaxAmountCommand and expose through TaxController * Add PreviewTaxAmountCommandTests * Run dotnet format
This commit is contained in:
@ -4,7 +4,9 @@ using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Commands;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
namespace Bit.Core.Billing.Extensions;
|
||||
|
||||
@ -24,5 +26,6 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<IAutomaticTaxFactory, AutomaticTaxFactory>();
|
||||
services.AddLicenseServices();
|
||||
services.AddPricingClient();
|
||||
services.AddTransient<IPreviewTaxAmountCommand, PreviewTaxAmountCommand>();
|
||||
}
|
||||
}
|
||||
|
36
src/Core/Billing/Models/BillingCommandResult.cs
Normal file
36
src/Core/Billing/Models/BillingCommandResult.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record BadRequest(string TranslationKey)
|
||||
{
|
||||
public static BadRequest TaxIdNumberInvalid => new(BillingErrorTranslationKeys.TaxIdInvalid);
|
||||
public static BadRequest TaxLocationInvalid => new(BillingErrorTranslationKeys.CustomerTaxLocationInvalid);
|
||||
public static BadRequest UnknownTaxIdType => new(BillingErrorTranslationKeys.UnknownTaxIdType);
|
||||
}
|
||||
|
||||
public record Unhandled(string TranslationKey = BillingErrorTranslationKeys.UnhandledError);
|
||||
|
||||
public class BillingCommandResult<T> : OneOfBase<T, BadRequest, Unhandled>
|
||||
{
|
||||
private BillingCommandResult(OneOf<T, BadRequest, Unhandled> input) : base(input) { }
|
||||
|
||||
public static implicit operator BillingCommandResult<T>(T output) => new(output);
|
||||
public static implicit operator BillingCommandResult<T>(BadRequest badRequest) => new(badRequest);
|
||||
public static implicit operator BillingCommandResult<T>(Unhandled unhandled) => new(unhandled);
|
||||
}
|
||||
|
||||
public static class BillingErrorTranslationKeys
|
||||
{
|
||||
// "The tax ID number you provided was invalid. Please try again or contact support."
|
||||
public const string TaxIdInvalid = "taxIdInvalid";
|
||||
|
||||
// "Your location wasn't recognized. Please ensure your country and postal code are valid and try again."
|
||||
public const string CustomerTaxLocationInvalid = "customerTaxLocationInvalid";
|
||||
|
||||
// "Something went wrong with your request. Please contact support."
|
||||
public const string UnhandledError = "unhandledBillingError";
|
||||
|
||||
// "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support."
|
||||
public const string UnknownTaxIdType = "unknownTaxIdType";
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record PaymentMethod(
|
||||
long AccountCredit,
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
|
||||
#nullable enable
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Sales;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
|
@ -6,6 +6,8 @@ using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
|
@ -2,7 +2,9 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -2,6 +2,8 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
147
src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
Normal file
147
src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs
Normal file
@ -0,0 +1,147 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.Commands;
|
||||
|
||||
public interface IPreviewTaxAmountCommand
|
||||
{
|
||||
Task<BillingCommandResult<decimal>> Run(OrganizationTrialParameters parameters);
|
||||
}
|
||||
|
||||
public class PreviewTaxAmountCommand(
|
||||
ILogger<PreviewTaxAmountCommand> logger,
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ITaxService taxService) : IPreviewTaxAmountCommand
|
||||
{
|
||||
public async Task<BillingCommandResult<decimal>> Run(OrganizationTrialParameters parameters)
|
||||
{
|
||||
var (planType, productType, taxInformation) = parameters;
|
||||
|
||||
var plan = await pricingClient.GetPlanOrThrow(planType);
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
Currency = "usd",
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInformation.Country,
|
||||
PostalCode = taxInformation.PostalCode
|
||||
}
|
||||
},
|
||||
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||
{
|
||||
Items = [
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = plan.HasNonSeatBasedPasswordManagerPlan() ? plan.PasswordManager.StripePlanId : plan.PasswordManager.StripeSeatPlanId,
|
||||
Quantity = 1
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
if (productType == ProductType.SecretsManager)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Price = plan.SecretsManager.StripeSeatPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(taxInformation.TaxId))
|
||||
{
|
||||
var taxIdType = taxService.GetStripeTaxCode(
|
||||
taxInformation.Country,
|
||||
taxInformation.TaxId);
|
||||
|
||||
if (string.IsNullOrEmpty(taxIdType))
|
||||
{
|
||||
return BadRequest.UnknownTaxIdType;
|
||||
}
|
||||
|
||||
options.CustomerDetails.TaxIds = [
|
||||
new InvoiceCustomerDetailsTaxIdOptions
|
||||
{
|
||||
Type = taxIdType,
|
||||
Value = taxInformation.TaxId
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (planType.GetProductTier() == ProductTierType.Families)
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions
|
||||
{
|
||||
Enabled = options.CustomerDetails.Address.Country == "US" ||
|
||||
options.CustomerDetails.TaxIds is [_, ..]
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
return Convert.ToDecimal(invoice.Tax) / 100;
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError.Code ==
|
||||
StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
|
||||
{
|
||||
return BadRequest.TaxLocationInvalid;
|
||||
}
|
||||
catch (StripeException stripeException) when (stripeException.StripeError.Code ==
|
||||
StripeConstants.ErrorCodes.TaxIdInvalid)
|
||||
{
|
||||
return BadRequest.TaxIdNumberInvalid;
|
||||
}
|
||||
catch (StripeException stripeException)
|
||||
{
|
||||
logger.LogError(stripeException, "Stripe responded with an error during {Operation}. Code: {Code}", nameof(PreviewTaxAmountCommand), stripeException.StripeError.Code);
|
||||
return new Unhandled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Command Parameters
|
||||
|
||||
public record OrganizationTrialParameters
|
||||
{
|
||||
public required PlanType PlanType { get; set; }
|
||||
public required ProductType ProductType { get; set; }
|
||||
public required TaxInformationDTO TaxInformation { get; set; }
|
||||
|
||||
public void Deconstruct(
|
||||
out PlanType planType,
|
||||
out ProductType productType,
|
||||
out TaxInformationDTO taxInformation)
|
||||
{
|
||||
planType = PlanType;
|
||||
productType = ProductType;
|
||||
taxInformation = TaxInformation;
|
||||
}
|
||||
|
||||
public record TaxInformationDTO
|
||||
{
|
||||
public required string Country { get; set; }
|
||||
public required string PostalCode { get; set; }
|
||||
public string? TaxId { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
namespace Bit.Core.Billing.Tax.Models;
|
||||
|
||||
public class TaxIdType
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
namespace Bit.Core.Billing.Tax.Models;
|
||||
|
||||
public record TaxInformation(
|
||||
string Country,
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class PreviewIndividualInvoiceRequestBody
|
||||
{
|
@ -2,7 +2,7 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class PreviewOrganizationInvoiceRequestBody
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Core.Billing.Models.Api.Requests;
|
||||
namespace Bit.Core.Billing.Tax.Requests;
|
||||
|
||||
public class TaxInformationRequestModel
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Models.Api.Responses;
|
||||
namespace Bit.Core.Billing.Tax.Responses;
|
||||
|
||||
public record PreviewInvoiceResponseModel(
|
||||
decimal EffectiveTaxRate,
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for defining the correct automatic tax strategy for either personal use of business use.
|
@ -1,7 +1,7 @@
|
||||
#nullable enable
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
public interface IAutomaticTaxStrategy
|
||||
{
|
@ -1,4 +1,4 @@
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services;
|
||||
|
||||
public interface ITaxService
|
||||
{
|
@ -5,7 +5,7 @@ using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class AutomaticTaxFactory(
|
||||
IFeatureService featureService,
|
@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
|
||||
{
|
@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
public class TaxService : ITaxService
|
||||
{
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.Api.Responses;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
|
@ -3,14 +3,15 @@ using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||
using Bit.Core.Billing.Models.Api.Responses;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
Reference in New Issue
Block a user