From b877c252347132e6db618a71a91bff4ed98af6da Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Fri, 4 Dec 2020 12:05:16 -0500 Subject: [PATCH] Implemented tax collection for subscriptions (#1017) * Implemented tax collection for subscriptions * Cleanup for Sales Tax * Cleanup for Sales Tax * Changes a constraint to an index for checking purposes * Added and implemented a ReadById method for TaxRate * Code review fixes for Tax Rate implementation * Code review fixes for Tax Rate implementation * Made the SalesTax migration script rerunnable --- src/Admin/Controllers/ToolsController.cs | 106 +++++- src/Admin/Models/TaxRateAddEditModel.cs | 11 + src/Admin/Models/TaxRatesModel.cs | 9 + src/Admin/Views/Shared/_Layout.cshtml | 3 + src/Admin/Views/Tools/TaxRate.cshtml | 96 +++++ src/Admin/Views/Tools/TaxRateAddEdit.cshtml | 356 ++++++++++++++++++ src/Api/Controllers/PlansController.cs | 16 + .../OrganizationUpgradeRequestModel.cs | 9 +- .../Api/Response/TaxRateResponseModel.cs | 29 ++ .../Models/Business/OrganizationUpgrade.cs | 1 + .../Business/SubscriptionCreateOptions.cs | 85 +++++ src/Core/Models/Business/TaxInfo.cs | 1 + src/Core/Models/Table/TaxRate.cs | 18 + src/Core/Repositories/ITaxRateRepository.cs | 14 + .../SqlServer/TaxRateRepository.cs | 70 ++++ src/Core/Services/IPaymentService.cs | 5 +- .../Implementations/OrganizationService.cs | 2 +- .../Implementations/StripePaymentService.cs | 163 ++++---- .../Utilities/ServiceCollectionExtensions.cs | 1 + .../dbo/Stored Procedures/TaxRate_Archive.sql | 13 + .../dbo/Stored Procedures/TaxRate_Create.sql | 30 ++ .../TaxRate_ReadAllActive.sql | 9 + .../Stored Procedures/TaxRate_ReadById.sql | 9 + .../TaxRate_ReadByLocation.sql | 12 + .../dbo/Stored Procedures/TaxRate_Search.sql | 14 + src/Sql/dbo/Tables/TaxRate.sql | 14 + .../DbScripts/2020-11-16_00_SalesTax.sql | 149 ++++++++ 27 files changed, 1157 insertions(+), 88 deletions(-) create mode 100644 src/Admin/Models/TaxRateAddEditModel.cs create mode 100644 src/Admin/Models/TaxRatesModel.cs create mode 100644 src/Admin/Views/Tools/TaxRate.cshtml create mode 100644 src/Admin/Views/Tools/TaxRateAddEdit.cshtml create mode 100644 src/Core/Models/Api/Response/TaxRateResponseModel.cs create mode 100644 src/Core/Models/Business/SubscriptionCreateOptions.cs create mode 100644 src/Core/Models/Table/TaxRate.cs create mode 100644 src/Core/Repositories/ITaxRateRepository.cs create mode 100644 src/Core/Repositories/SqlServer/TaxRateRepository.cs create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_Archive.sql create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_Create.sql create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_ReadAllActive.sql create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_ReadById.sql create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_ReadByLocation.sql create mode 100644 src/Sql/dbo/Stored Procedures/TaxRate_Search.sql create mode 100644 src/Sql/dbo/Tables/TaxRate.sql create mode 100644 util/Migrator/DbScripts/2020-11-16_00_SalesTax.sql diff --git a/src/Admin/Controllers/ToolsController.cs b/src/Admin/Controllers/ToolsController.cs index 951d533e9b..8a5da7355c 100644 --- a/src/Admin/Controllers/ToolsController.cs +++ b/src/Admin/Controllers/ToolsController.cs @@ -26,6 +26,8 @@ namespace Bit.Admin.Controllers private readonly ITransactionRepository _transactionRepository; private readonly IInstallationRepository _installationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IPaymentService _paymentService; + private readonly ITaxRateRepository _taxRateRepository; public ToolsController( GlobalSettings globalSettings, @@ -34,7 +36,9 @@ namespace Bit.Admin.Controllers IUserService userService, ITransactionRepository transactionRepository, IInstallationRepository installationRepository, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + ITaxRateRepository taxRateRepository, + IPaymentService paymentService) { _globalSettings = globalSettings; _organizationRepository = organizationRepository; @@ -43,6 +47,8 @@ namespace Bit.Admin.Controllers _transactionRepository = transactionRepository; _installationRepository = installationRepository; _organizationUserRepository = organizationUserRepository; + _taxRateRepository = taxRateRepository; + _paymentService = paymentService; } public IActionResult ChargeBraintree() @@ -264,5 +270,103 @@ namespace Bit.Admin.Controllers throw new Exception("No license to generate."); } } + + public async Task TaxRate(int page = 1, int count = 25) + { + if (page < 1) + { + page = 1; + } + + if (count < 1) + { + count = 1; + } + + var skip = (page - 1) * count; + var rates = await _taxRateRepository.SearchAsync(skip, count); + return View(new TaxRatesModel + { + Items = rates.ToList(), + Page = page, + Count = count + }); + } + + public async Task TaxRateAddEdit(string stripeTaxRateId = null) + { + if (string.IsNullOrWhiteSpace(stripeTaxRateId)) + { + return View(new TaxRateAddEditModel()); + } + + var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId); + var model = new TaxRateAddEditModel() + { + StripeTaxRateId = stripeTaxRateId, + Country = rate.Country, + State = rate.State, + PostalCode = rate.PostalCode, + Rate = rate.Rate + }; + + return View(model); + } + + [HttpPost] + public async Task TaxRateAddEdit(TaxRateAddEditModel model) + { + var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode }); + if (existingRateCheck.Any()) + { + ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination."); + } + + if (!ModelState.IsValid) + { + return View(model); + } + + var taxRate = new TaxRate() + { + Id = model.StripeTaxRateId, + Country = model.Country, + State = model.State, + PostalCode = model.PostalCode, + Rate = model.Rate + }; + + if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId)) + { + await _paymentService.UpdateTaxRateAsync(taxRate); + } + else + { + await _paymentService.CreateTaxRateAsync(taxRate); + } + + return RedirectToAction("TaxRate"); + } + + [HttpPost] + public async Task TaxRateUpdate(TaxRate model) + { + if (!string.IsNullOrWhiteSpace(model.Id)) + { + await _paymentService.UpdateTaxRateAsync(model); + } + + return RedirectToAction("TaxRate"); + } + + public async Task TaxRateArchive(string stripeTaxRateId) + { + if (!string.IsNullOrWhiteSpace(stripeTaxRateId)) + { + await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId }); + } + + return RedirectToAction("TaxRate"); + } } } diff --git a/src/Admin/Models/TaxRateAddEditModel.cs b/src/Admin/Models/TaxRateAddEditModel.cs new file mode 100644 index 0000000000..7d098cd863 --- /dev/null +++ b/src/Admin/Models/TaxRateAddEditModel.cs @@ -0,0 +1,11 @@ +namespace Bit.Admin.Models +{ + public class TaxRateAddEditModel + { + public string StripeTaxRateId { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public decimal Rate { get; set; } + } +} diff --git a/src/Admin/Models/TaxRatesModel.cs b/src/Admin/Models/TaxRatesModel.cs new file mode 100644 index 0000000000..4af8540fa7 --- /dev/null +++ b/src/Admin/Models/TaxRatesModel.cs @@ -0,0 +1,9 @@ +using Bit.Core.Models.Table; + +namespace Bit.Admin.Models +{ + public class TaxRatesModel: PagedModel + { + public string Message { get; set; } + } +} diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index aaa4079ffb..cf56fafa65 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -58,6 +58,9 @@ Generate License + + Manage Tax Rates +