From b7a500eb63f8e88c18082216f10e84251b981a3c Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:49:27 -0400 Subject: [PATCH 1/3] combined tax updates with other operations --- src/Api/Controllers/AccountsController.cs | 12 +++++++++- .../Controllers/OrganizationsController.cs | 13 +++++++++-- .../Request/Accounts/PremiumRequestModel.cs | 15 +++++++++++-- .../Models/Api/Request/PaymentRequestModel.cs | 2 +- src/Core/Services/IOrganizationService.cs | 3 ++- src/Core/Services/IPaymentService.cs | 2 +- src/Core/Services/IUserService.cs | 3 ++- .../Implementations/OrganizationService.cs | 3 ++- .../Implementations/StripePaymentService.cs | 22 +++++++++++++++++-- .../Services/Implementations/UserService.cs | 5 +++-- 10 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index f3457cc176..168af1a4a1 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -467,6 +467,10 @@ namespace Bit.Api.Controllers { license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); } + else if (!valid && !_globalSettings.SelfHosted) + { + throw new BadRequestException("Country is required."); + } if (!valid || (_globalSettings.SelfHosted && license == null)) { @@ -474,7 +478,8 @@ namespace Bit.Api.Controllers } var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken, - model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license); + model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, + model.Country, model.PostalCode); var profile = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); return new PaymentResponseModel { @@ -534,6 +539,11 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } + await _paymentService.SaveTaxInfoAsync(user, new TaxInfo + { + BillingAddressCountry = model.Country, + BillingAddressPostalCode = model.PostalCode, + }); await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value); } diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index a4dcd4c916..d9f2b545fa 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -208,9 +208,18 @@ namespace Bit.Api.Controllers { throw new NotFoundException(); } - + await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken, - model.PaymentMethodType.Value); + model.PaymentMethodType.Value, new TaxInfo + { + BillingAddressLine1 = model.Line1, + BillingAddressLine2 = model.Line2, + BillingAddressState = model.State, + BillingAddressCity = model.City, + BillingAddressPostalCode = model.PostalCode, + BillingAddressCountry = model.Country, + TaxIdNumber = model.TaxId, + }); } [HttpPost("{id}/upgrade")] diff --git a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs index 8fd4c6329d..910f56167e 100644 --- a/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/PremiumRequestModel.cs @@ -13,11 +13,17 @@ namespace Bit.Core.Models.Api [Range(0, 99)] public short? AdditionalStorageGb { get; set; } public IFormFile License { get; set; } + public string Country { get; set; } + public string PostalCode { get; set; } public bool Validate(GlobalSettings globalSettings) { - return (License == null && !globalSettings.SelfHosted) || - (License != null && globalSettings.SelfHosted); + if (!(License == null && !globalSettings.SelfHosted) || + (License != null && globalSettings.SelfHosted)) + { + return false; + } + return globalSettings.SelfHosted || !string.IsNullOrWhiteSpace(Country); } public IEnumerable Validate(ValidationContext validationContext) @@ -27,6 +33,11 @@ namespace Bit.Core.Models.Api { yield return new ValidationResult("Payment token or license is required."); } + if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode)) + { + yield return new ValidationResult("Zip / postal code is required.", + new string[] { nameof(PostalCode) }); + } } } } diff --git a/src/Core/Models/Api/Request/PaymentRequestModel.cs b/src/Core/Models/Api/Request/PaymentRequestModel.cs index 7834805779..84b5dc9759 100644 --- a/src/Core/Models/Api/Request/PaymentRequestModel.cs +++ b/src/Core/Models/Api/Request/PaymentRequestModel.cs @@ -3,7 +3,7 @@ using Bit.Core.Enums; namespace Bit.Core.Models.Api { - public class PaymentRequestModel + public class PaymentRequestModel : OrganizationTaxInfoUpdateRequestModel { [Required] public PaymentMethodType? PaymentMethodType { get; set; } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 6b3d64027e..dba1bba8e3 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -10,7 +10,8 @@ namespace Bit.Core.Services { public interface IOrganizationService { - Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType); + Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType, + TaxInfo taxInfo); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task ReinstateSubscriptionAsync(Guid organizationId); Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade); diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 2bdb8f4e1f..166fb23c6c 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -14,7 +14,7 @@ namespace Bit.Core.Services Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, - short additionalStorageGb); + short additionalStorageGb, string country, string postalCode); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, bool skipInAppPurchaseCheck = false); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 5f62c4d4a0..5e51183c3e 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -46,7 +46,8 @@ namespace Bit.Core.Services Task DeleteAsync(User user, string token); Task SendDeleteConfirmationAsync(string email); Task> SignUpPremiumAsync(User user, string paymentToken, - PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license); + PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license, + string country, string postalCode); Task IapCheckAsync(User user, PaymentMethodType paymentMethodType); Task UpdateLicenseAsync(User user, UserLicense license); Task AdjustStorageAsync(User user, short storageAdjustmentGb); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 4fa4388bfa..a0686d4eb8 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -75,7 +75,7 @@ namespace Bit.Core.Services } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, - PaymentMethodType paymentMethodType) + PaymentMethodType paymentMethodType, TaxInfo taxInfo) { var organization = await GetOrgById(organizationId); if (organization == null) @@ -83,6 +83,7 @@ namespace Bit.Core.Services throw new NotFoundException(); } + await _paymentService.SaveTaxInfoAsync(organization, taxInfo); var updated = await _paymentService.UpdatePaymentMethodAsync(organization, paymentMethodType, paymentToken); if (updated) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 4e954b50e5..357c8ea83d 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -345,7 +345,7 @@ namespace Bit.Core.Services } public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, - string paymentToken, short additionalStorageGb) + string paymentToken, short additionalStorageGb, string country, string postalCode) { if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken)) { @@ -463,10 +463,28 @@ namespace Bit.Core.Services InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = stipeCustomerPaymentMethodId - } + }, + Address = new AddressOptions + { + Line1 = string.Empty, + Country = country, + PostalCode = postalCode, + }, }); createdStripeCustomer = true; } + else if (customer != null) + { + await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions + { + Address = new AddressOptions + { + Line1 = string.Empty, + Country = country, + PostalCode = postalCode, + } + }); + } if (customer == null) { diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index c666b3b18c..0a2a7452f1 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -703,7 +703,8 @@ namespace Bit.Core.Services } public async Task> SignUpPremiumAsync(User user, string paymentToken, - PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license) + PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license, + string country, string postalCode) { if (user.Premium) { @@ -742,7 +743,7 @@ namespace Bit.Core.Services else { paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, - paymentToken, additionalStorageGb); + paymentToken, additionalStorageGb, country, postalCode); } user.Premium = true; From 1b027cab5988fada576a63a796654e79d603a621 Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:02:38 -0400 Subject: [PATCH 2/3] Remove erroneous whitespace --- src/Api/Controllers/OrganizationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index d9f2b545fa..947396fc2c 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -208,7 +208,7 @@ namespace Bit.Api.Controllers { throw new NotFoundException(); } - + await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken, model.PaymentMethodType.Value, new TaxInfo { From 0f28ac45f9e052dc36259157c8c84a019f5caa3f Mon Sep 17 00:00:00 2001 From: Chad Scharf <3904944+cscharf@users.noreply.github.com> Date: Thu, 18 Jun 2020 10:41:55 -0400 Subject: [PATCH 3/3] Consistency on TaxInfo use in service params --- src/Api/Controllers/AccountsController.cs | 21 +++++---- src/Core/Services/IPaymentService.cs | 4 +- src/Core/Services/IUserService.cs | 4 +- .../Implementations/StripePaymentService.cs | 44 +++++++++++-------- .../Services/Implementations/UserService.cs | 8 ++-- 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 168af1a4a1..cb4630d007 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -467,7 +467,8 @@ namespace Bit.Api.Controllers { license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); } - else if (!valid && !_globalSettings.SelfHosted) + + if (!valid && !_globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country)) { throw new BadRequestException("Country is required."); } @@ -479,7 +480,11 @@ namespace Bit.Api.Controllers var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken, model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, - model.Country, model.PostalCode); + new TaxInfo + { + BillingAddressCountry = model.Country, + BillingAddressPostalCode = model.PostalCode, + }); var profile = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); return new PaymentResponseModel { @@ -539,12 +544,12 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } - await _paymentService.SaveTaxInfoAsync(user, new TaxInfo - { - BillingAddressCountry = model.Country, - BillingAddressPostalCode = model.PostalCode, - }); - await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value); + await _userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType.Value, + new TaxInfo + { + BillingAddressCountry = model.Country, + BillingAddressPostalCode = model.PostalCode, + }); } [HttpPost("storage")] diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 166fb23c6c..b9b688ff09 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -14,13 +14,13 @@ namespace Bit.Core.Services Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, - short additionalStorageGb, string country, string postalCode); + short additionalStorageGb, TaxInfo taxInfo); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, bool skipInAppPurchaseCheck = false); Task ReinstateSubscriptionAsync(ISubscriber subscriber); Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, bool allowInAppPurchases = false); + string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null); Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount); Task GetBillingAsync(ISubscriber subscriber); Task GetSubscriptionAsync(ISubscriber subscriber); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 5e51183c3e..ccea70a45b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -47,11 +47,11 @@ namespace Bit.Core.Services Task SendDeleteConfirmationAsync(string email); Task> SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license, - string country, string postalCode); + TaxInfo taxInfo); Task IapCheckAsync(User user, PaymentMethodType paymentMethodType); Task UpdateLicenseAsync(User user, UserLicense license); Task AdjustStorageAsync(User user, short storageAdjustmentGb); - Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType); + Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, TaxInfo taxInfo); Task CancelPremiumAsync(User user, bool? endOfPeriod = null, bool accountDelete = false); Task ReinstatePremiumAsync(User user); Task EnablePremiumAsync(Guid userId, DateTime? expirationDate); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 357c8ea83d..2326544826 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -345,7 +345,7 @@ namespace Bit.Core.Services } public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, - string paymentToken, short additionalStorageGb, string country, string postalCode) + string paymentToken, short additionalStorageGb, TaxInfo taxInfo) { if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken)) { @@ -393,7 +393,7 @@ namespace Bit.Core.Services { try { - await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, true); + await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, true, taxInfo); } catch (Exception e) { @@ -467,24 +467,12 @@ namespace Bit.Core.Services Address = new AddressOptions { Line1 = string.Empty, - Country = country, - PostalCode = postalCode, + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode, }, }); createdStripeCustomer = true; } - else if (customer != null) - { - await customerService.UpdateAsync(customer.Id, new CustomerUpdateOptions - { - Address = new AddressOptions - { - Line1 = string.Empty, - Country = country, - PostalCode = postalCode, - } - }); - } if (customer == null) { @@ -1116,7 +1104,7 @@ namespace Bit.Core.Services } public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, bool allowInAppPurchases = false) + string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null) { if (subscriber == null) { @@ -1304,7 +1292,16 @@ namespace Bit.Core.Services InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = stipeCustomerPaymentMethodId - } + }, + Address = taxInfo == null ? null : new AddressOptions + { + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode, + Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, + Line2 = taxInfo.BillingAddressLine2, + City = taxInfo.BillingAddressCity, + State = taxInfo.BillingAddressState, + }, }); subscriber.Gateway = GatewayType.Stripe; @@ -1363,7 +1360,16 @@ namespace Bit.Core.Services InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = defaultPaymentMethodId - } + }, + Address = taxInfo == null ? null : new AddressOptions + { + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode, + Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, + Line2 = taxInfo.BillingAddressLine2, + City = taxInfo.BillingAddressCity, + State = taxInfo.BillingAddressState, + }, }); } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 0a2a7452f1..cfbb648883 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -704,7 +704,7 @@ namespace Bit.Core.Services public async Task> SignUpPremiumAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, short additionalStorageGb, UserLicense license, - string country, string postalCode) + TaxInfo taxInfo) { if (user.Premium) { @@ -743,7 +743,7 @@ namespace Bit.Core.Services else { paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, - paymentToken, additionalStorageGb, country, postalCode); + paymentToken, additionalStorageGb, taxInfo); } user.Premium = true; @@ -845,14 +845,14 @@ namespace Bit.Core.Services return secret; } - public async Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType) + public async Task ReplacePaymentMethodAsync(User user, string paymentToken, PaymentMethodType paymentMethodType, TaxInfo taxInfo) { if (paymentToken.StartsWith("btok_")) { throw new BadRequestException("Invalid token."); } - var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken); + var updated = await _paymentService.UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo: taxInfo); if (updated) { await SaveUserAsync(user);