mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
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
This commit is contained in:
@ -240,7 +240,7 @@ namespace Bit.Core.Services
|
||||
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
|
||||
{
|
||||
paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, newPlan,
|
||||
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon);
|
||||
upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo);
|
||||
success = string.IsNullOrWhiteSpace(paymentIntentClientSecret);
|
||||
}
|
||||
else
|
||||
|
@ -10,6 +10,8 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Bit.Billing.Models;
|
||||
using StripeTaxRate = Stripe.TaxRate;
|
||||
using TaxRate = Bit.Core.Models.Table.TaxRate;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
@ -25,13 +27,15 @@ namespace Bit.Core.Services
|
||||
private readonly IAppleIapService _appleIapService;
|
||||
private readonly ILogger<StripePaymentService> _logger;
|
||||
private readonly Braintree.BraintreeGateway _btGateway;
|
||||
private readonly ITaxRateRepository _taxRateRepository;
|
||||
|
||||
public StripePaymentService(
|
||||
ITransactionRepository transactionRepository,
|
||||
IUserRepository userRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IAppleIapService appleIapService,
|
||||
ILogger<StripePaymentService> logger)
|
||||
ILogger<StripePaymentService> logger,
|
||||
ITaxRateRepository taxRateRepository)
|
||||
{
|
||||
_btGateway = new Braintree.BraintreeGateway
|
||||
{
|
||||
@ -45,6 +49,7 @@ namespace Bit.Core.Services
|
||||
_userRepository = userRepository;
|
||||
_appleIapService = appleIapService;
|
||||
_logger = logger;
|
||||
_taxRateRepository = taxRateRepository;
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
@ -98,52 +103,24 @@ namespace Bit.Core.Services
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
|
||||
{
|
||||
OffSession = true,
|
||||
TrialPeriodDays = plan.TrialPeriodDays,
|
||||
Items = new List<SubscriptionItemOptions>(),
|
||||
Metadata = new Dictionary<string, string>
|
||||
var taxRateSearch = new TaxRate()
|
||||
{
|
||||
[org.GatewayIdField()] = org.Id.ToString()
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode
|
||||
};
|
||||
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);
|
||||
|
||||
// should only be one tax rate per country/zip combo
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null)
|
||||
{
|
||||
taxInfo.StripeTaxRateId = taxRate.Id;
|
||||
}
|
||||
};
|
||||
|
||||
if (plan.StripePlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripePlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripeSeatPlanId,
|
||||
Quantity = additionalSeats
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripeStoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripePremiumAccessPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
|
||||
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
@ -224,7 +201,7 @@ namespace Bit.Core.Services
|
||||
}
|
||||
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan,
|
||||
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon)
|
||||
short additionalStorageGb, short additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
{
|
||||
@ -241,52 +218,24 @@ namespace Bit.Core.Services
|
||||
throw new GatewayException("Could not find customer payment profile.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Items = new List<SubscriptionItemOptions>(),
|
||||
Metadata = new Dictionary<string, string>
|
||||
var taxRateSearch = new TaxRate()
|
||||
{
|
||||
[org.GatewayIdField()] = org.Id.ToString()
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode
|
||||
};
|
||||
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);
|
||||
|
||||
// should only be one tax rate per country/zip combo
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null)
|
||||
{
|
||||
taxInfo.StripeTaxRateId = taxRate.Id;
|
||||
}
|
||||
};
|
||||
|
||||
if (plan.StripePlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripePlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalSeats > 0 && plan.StripeSeatPlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripeSeatPlanId,
|
||||
Quantity = additionalSeats
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripeStoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = plan.StripePremiumAccessPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon);
|
||||
var stripePaymentMethod = false;
|
||||
var paymentMethodType = PaymentMethodType.Credit;
|
||||
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
|
||||
@ -1632,6 +1581,52 @@ namespace Bit.Core.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
|
||||
{
|
||||
var stripeTaxRateOptions = new TaxRateCreateOptions()
|
||||
{
|
||||
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
|
||||
Inclusive = false,
|
||||
Percentage = taxRate.Rate,
|
||||
Active = true
|
||||
};
|
||||
var taxRateService = new TaxRateService();
|
||||
var stripeTaxRate = taxRateService.Create(stripeTaxRateOptions);
|
||||
taxRate.Id = stripeTaxRate.Id;
|
||||
await _taxRateRepository.CreateAsync(taxRate);
|
||||
return taxRate;
|
||||
}
|
||||
|
||||
public async Task UpdateTaxRateAsync(TaxRate taxRate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(taxRate.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await ArchiveTaxRateAsync(taxRate);
|
||||
await CreateTaxRateAsync(taxRate);
|
||||
}
|
||||
|
||||
public async Task ArchiveTaxRateAsync(TaxRate taxRate)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(taxRate.Id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var stripeTaxRateService = new TaxRateService();
|
||||
var updatedStripeTaxRate = await stripeTaxRateService.UpdateAsync(
|
||||
taxRate.Id,
|
||||
new TaxRateUpdateOptions() { Active = false }
|
||||
);
|
||||
if (!updatedStripeTaxRate.Active)
|
||||
{
|
||||
taxRate.Active = false;
|
||||
await _taxRateRepository.ArchiveAsync(taxRate);
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
{
|
||||
var paymentMethodService = new PaymentMethodService();
|
||||
|
Reference in New Issue
Block a user