1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

API updates for tax info collection

This commit is contained in:
Chad Scharf 2020-06-08 17:40:18 -04:00
parent cad7cf0200
commit d88838f19e
11 changed files with 411 additions and 5 deletions

View File

@ -618,5 +618,38 @@ namespace Bit.Api.Controllers
return token; return token;
} }
[HttpGet("tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<TaxInfoResponseModel> GetTaxInfo()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var taxInfo = await _paymentService.GetTaxInfoAsync(user);
return new TaxInfoResponseModel(taxInfo);
}
[HttpPut("tax")]
[HttpPost("tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PutTaxInfo([FromBody]TaxInfoUpdateRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var taxInfo = new TaxInfo
{
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
};
await _paymentService.SaveTaxInfoAsync(user, taxInfo);
}
} }
} }

View File

@ -462,5 +462,55 @@ namespace Bit.Api.Controllers
return response; return response;
} }
} }
[HttpGet("{id}/tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<TaxInfoResponseModel> GetTaxInfo(string id)
{
var orgIdGuid = new Guid(id);
if (!_currentContext.OrganizationOwner(orgIdGuid))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var taxInfo = await _paymentService.GetTaxInfoAsync(organization);
return new TaxInfoResponseModel(taxInfo);
}
[HttpPut("{id}/tax")]
[HttpPost("{id}/tax")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PutTaxInfo(string id, [FromBody]OrganizationTaxInfoUpdateRequestModel model)
{
var orgIdGuid = new Guid(id);
if (!_currentContext.OrganizationOwner(orgIdGuid))
{
throw new NotFoundException();
}
var organization = await _organizationRepository.GetByIdAsync(orgIdGuid);
if (organization == null)
{
throw new NotFoundException();
}
var taxInfo = new TaxInfo
{
TaxIdNumber = model.TaxId,
BillingAddressLine1 = model.Line1,
BillingAddressLine2 = model.Line2,
BillingAddressCity = model.City,
BillingAddressState = model.State,
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
};
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
}
} }
} }

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class TaxInfoUpdateRequestModel : IValidatableObject
{
[Required]
public string Country { get; set; }
public string PostalCode { get; set; }
public virtual IEnumerable<ValidationResult> Validate (ValidationContext validationContext)
{
if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(PostalCode) });
}
}
}
}

View File

@ -31,6 +31,15 @@ namespace Bit.Core.Models.Api
[EncryptedString] [EncryptedString]
[EncryptedStringLength(1000)] [EncryptedStringLength(1000)]
public string CollectionName { get; set; } public string CollectionName { get; set; }
public string TaxIdNumber { get; set; }
public string BillingAddressLine1 { get; set; }
public string BillingAddressLine2 { get; set; }
public string BillingAddressCity { get; set; }
public string BillingAddressState { get; set; }
public string BillingAddressPostalCode { get; set; }
[Required]
[StringLength(2)]
public string BillingAddressCountry { get; set; }
public virtual OrganizationSignup ToOrganizationSignup(User user) public virtual OrganizationSignup ToOrganizationSignup(User user)
{ {
@ -47,7 +56,17 @@ namespace Bit.Core.Models.Api
PremiumAccessAddon = PremiumAccessAddon, PremiumAccessAddon = PremiumAccessAddon,
BillingEmail = BillingEmail, BillingEmail = BillingEmail,
BusinessName = BusinessName, BusinessName = BusinessName,
CollectionName = CollectionName CollectionName = CollectionName,
TaxInfo = new TaxInfo
{
TaxIdNumber = TaxIdNumber,
BillingAddressLine1 = BillingAddressLine1,
BillingAddressLine2 = BillingAddressLine2,
BillingAddressCity = BillingAddressCity,
BillingAddressState = BillingAddressState,
BillingAddressPostalCode = BillingAddressPostalCode,
BillingAddressCountry = BillingAddressCountry,
},
}; };
} }
@ -62,6 +81,17 @@ namespace Bit.Core.Models.Api
yield return new ValidationResult("Payment method type required.", yield return new ValidationResult("Payment method type required.",
new string[] { nameof(PaymentMethodType) }); new string[] { nameof(PaymentMethodType) });
} }
if (PlanType != PlanType.Free && string.IsNullOrWhiteSpace(BillingAddressCountry))
{
yield return new ValidationResult("Country required.",
new string[] { nameof(BillingAddressCountry) });
}
if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(BillingAddressPostalCode) });
}
} }
} }
} }

View File

@ -0,0 +1,11 @@
namespace Bit.Core.Models.Api
{
public class OrganizationTaxInfoUpdateRequestModel : TaxInfoUpdateRequestModel
{
public string TaxId { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string City { get; set; }
public string State { get; set; }
}
}

View File

@ -0,0 +1,34 @@
using Bit.Core.Models.Business;
namespace Bit.Core.Models.Api
{
public class TaxInfoResponseModel
{
public TaxInfoResponseModel () { }
public TaxInfoResponseModel(TaxInfo taxInfo)
{
if (taxInfo == null)
{
return;
}
TaxIdNumber = taxInfo.TaxIdNumber;
TaxIdType = taxInfo.TaxIdType;
Line1 = taxInfo.BillingAddressLine1;
Line2 = taxInfo.BillingAddressLine2;
City = taxInfo.BillingAddressCity;
State = taxInfo.BillingAddressState;
PostalCode = taxInfo.BillingAddressPostalCode;
Country = taxInfo.BillingAddressCountry;
}
public string TaxIdNumber { get; set; }
public string TaxIdType { get; set; }
public string Line1 { get; set; }
public string Line2 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
}
}

View File

@ -12,5 +12,6 @@ namespace Bit.Core.Models.Business
public string CollectionName { get; set; } public string CollectionName { get; set; }
public PaymentMethodType? PaymentMethodType { get; set; } public PaymentMethodType? PaymentMethodType { get; set; }
public string PaymentToken { get; set; } public string PaymentToken { get; set; }
public TaxInfo TaxInfo { get; set; }
} }
} }

View File

@ -0,0 +1,140 @@
namespace Bit.Core.Models.Business
{
public class TaxInfo
{
private string _taxIdNumber = null;
private string _taxIdType = null;
public string TaxIdNumber
{
get => _taxIdNumber;
set
{
_taxIdNumber = value;
_taxIdType = null;
}
}
public string BillingAddressLine1 { get; set; }
public string BillingAddressLine2 { get; set; }
public string BillingAddressCity { get; set; }
public string BillingAddressState { get; set; }
public string BillingAddressPostalCode { get; set; }
public string BillingAddressCountry { get; set; } = "US";
public string TaxIdType
{
get
{
if (string.IsNullOrWhiteSpace(BillingAddressCountry) ||
string.IsNullOrWhiteSpace(TaxIdNumber))
return null;
if (!string.IsNullOrWhiteSpace(_taxIdType))
return _taxIdType;
switch (BillingAddressCountry)
{
case "AE":
_taxIdType = "ae_trn";
break;
case "AU":
_taxIdType = "au_abn";
break;
case "BR":
_taxIdType = "br_cnpj";
break;
case "CA":
// May break for those in Québec given the assumption of QST
if (BillingAddressState?.Contains("bec") ?? false)
_taxIdType = "ca_qst";
_taxIdType = "ca_bn";
break;
case "CL":
_taxIdType = "cl_tin";
break;
case "AT":
case "BE":
case "BG":
case "CY":
case "CZ":
case "DE":
case "DK":
case "EE":
case "ES":
case "FI":
case "FR":
case "GB":
case "GR":
case "HR":
case "HU":
case "IE":
case "IT":
case "LT":
case "LU":
case "LV":
case "MT":
case "NL":
case "PL":
case "PT":
case "RO":
case "SE":
case "SI":
case "SK":
_taxIdType = "eu_vat";
break;
case "HK":
_taxIdType = "hk_br";
break;
case "IN":
_taxIdType = "in_gst";
break;
case "JP":
_taxIdType = "jp_cn";
break;
case "KR":
_taxIdType = "kr_brn";
break;
case "LI":
_taxIdType = "li_uid";
break;
case "MX":
_taxIdType = "mx_rfc";
break;
case "MY":
_taxIdType = "my_sst";
break;
case "NO":
_taxIdType = "no_vat";
break;
case "NZ":
_taxIdType = "nz_gst";
break;
case "RU":
_taxIdType = "ru_inn";
break;
case "SA":
_taxIdType = "sa_vat";
break;
case "SG":
_taxIdType = "sg_gst";
break;
case "TH":
_taxIdType = "th_vat";
break;
case "TW":
_taxIdType = "tw_vat";
break;
case "US":
_taxIdType = "us_ein";
break;
case "ZA":
_taxIdType = "za_vat";
break;
default:
_taxIdType = null;
break;
}
return _taxIdType;
}
}
}
}

View File

@ -10,7 +10,7 @@ namespace Bit.Core.Services
Task CancelAndRecoverChargesAsync(ISubscriber subscriber); Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats, string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb, short additionalSeats,
bool premiumAccessAddon); bool premiumAccessAddon, TaxInfo taxInfo);
Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon); short additionalStorageGb, short additionalSeats, bool premiumAccessAddon);
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
@ -24,5 +24,7 @@ namespace Bit.Core.Services
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount); Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber); Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber); Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber);
Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber);
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
} }
} }

View File

@ -486,7 +486,7 @@ namespace Bit.Core.Services
{ {
await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value, await _paymentService.PurchaseOrganizationAsync(organization, signup.PaymentMethodType.Value,
signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats, signup.PaymentToken, plan, signup.AdditionalStorageGb, signup.AdditionalSeats,
signup.PremiumAccessAddon); signup.PremiumAccessAddon, signup.TaxInfo);
} }
return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true); return await SignUpAsync(organization, signup.Owner.Id, signup.OwnerKey, signup.CollectionName, true);

View File

@ -49,7 +49,7 @@ namespace Bit.Core.Services
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb, string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb,
short additionalSeats, bool premiumAccessAddon) short additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
{ {
var customerService = new CustomerService(); var customerService = new CustomerService();
@ -159,7 +159,21 @@ namespace Bit.Core.Services
InvoiceSettings = new CustomerInvoiceSettingsOptions InvoiceSettings = new CustomerInvoiceSettingsOptions
{ {
DefaultPaymentMethod = stipeCustomerPaymentMethodId DefaultPaymentMethod = stipeCustomerPaymentMethodId
} },
// TODO: Address info for zip code and country, optional other address info and tax ID
Address = new AddressOptions
{
Country = null,
PostalCode = null,
},
TaxIdData = new List<CustomerTaxIdDataOptions>
{
new CustomerTaxIdDataOptions
{
Type = "",
Value = null,
},
},
}); });
subCreateOptions.AddExpand("latest_invoice.payment_intent"); subCreateOptions.AddExpand("latest_invoice.payment_intent");
subCreateOptions.Customer = customer.Id; subCreateOptions.Customer = customer.Id;
@ -1501,6 +1515,76 @@ namespace Bit.Core.Services
return subscriptionInfo; return subscriptionInfo;
} }
public async Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber)
{
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{
var customerService = new CustomerService();
var customer = await customerService.GetAsync(subscriber.GatewayCustomerId);
if (customer == null)
{
return null;
}
var address = customer.Address;
var taxId = customer.TaxIds?.FirstOrDefault();
return new TaxInfo
{
TaxIdNumber = taxId?.Value,
BillingAddressLine1 = address?.Line1,
BillingAddressLine2 = address?.Line2,
BillingAddressCity = address?.City,
BillingAddressState = address?.State,
BillingAddressPostalCode = address?.PostalCode,
BillingAddressCountry = address?.Country,
};
}
return null;
}
public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo)
{
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
{
var customerService = new CustomerService();
var customer = await customerService.UpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions
{
Address = new AddressOptions
{
Line1 = taxInfo.BillingAddressLine1,
Line2 = taxInfo.BillingAddressLine2,
City = taxInfo.BillingAddressCity,
State = taxInfo.BillingAddressState,
PostalCode = taxInfo.BillingAddressPostalCode,
Country = taxInfo.BillingAddressCountry,
},
});
if (!subscriber.IsUser() && customer != null)
{
var taxIdService = new TaxIdService();
var taxId = customer.TaxIds?.FirstOrDefault();
if (taxId != null)
{
await taxIdService.DeleteAsync(customer.Id, taxId.Id);
}
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
{
await taxIdService.CreateAsync(customer.Id, new TaxIdCreateOptions
{
Type = taxInfo.TaxIdType,
Value = taxInfo.TaxIdNumber,
});
}
}
}
}
private PaymentMethod GetLatestCardPaymentMethod(string customerId) private PaymentMethod GetLatestCardPaymentMethod(string customerId)
{ {
var paymentMethodService = new PaymentMethodService(); var paymentMethodService = new PaymentMethodService();