diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index f3457cc176..cb4630d007 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -468,13 +468,23 @@ namespace Bit.Api.Controllers license = await ApiHelpers.ReadJsonFileFromBody(HttpContext, model.License); } + if (!valid && !_globalSettings.SelfHosted && string.IsNullOrWhiteSpace(model.Country)) + { + throw new BadRequestException("Country is required."); + } + if (!valid || (_globalSettings.SelfHosted && license == null)) { throw new BadRequestException("Invalid license."); } var result = await _userService.SignUpPremiumAsync(user, model.PaymentToken, - model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license); + model.PaymentMethodType.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, + new TaxInfo + { + BillingAddressCountry = model.Country, + BillingAddressPostalCode = model.PostalCode, + }); var profile = new ProfileResponseModel(user, null, await _userService.TwoFactorIsEnabledAsync(user)); return new PaymentResponseModel { @@ -534,7 +544,12 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } - 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/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index a4dcd4c916..947396fc2c 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -210,7 +210,16 @@ namespace Bit.Api.Controllers } 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..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); + 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 5f62c4d4a0..ccea70a45b 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -46,11 +46,12 @@ 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, + 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/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..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 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) { @@ -463,7 +463,13 @@ namespace Bit.Core.Services InvoiceSettings = new CustomerInvoiceSettingsOptions { DefaultPaymentMethod = stipeCustomerPaymentMethodId - } + }, + Address = new AddressOptions + { + Line1 = string.Empty, + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode, + }, }); createdStripeCustomer = true; } @@ -1098,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) { @@ -1286,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; @@ -1345,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 c666b3b18c..cfbb648883 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, + TaxInfo taxInfo) { if (user.Premium) { @@ -742,7 +743,7 @@ namespace Bit.Core.Services else { paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, - paymentToken, additionalStorageGb); + paymentToken, additionalStorageGb, taxInfo); } user.Premium = true; @@ -844,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);