mirror of
https://github.com/bitwarden/server
synced 2026-01-08 03:23:20 +00:00
[PM-29224] Remove unused billing endpoints and code paths (#6692)
* Remove unused endpoints and code paths * MOAR DELETE * Run dotnet format
This commit is contained in:
@@ -6,7 +6,6 @@ using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Stripe;
|
||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
@@ -64,16 +63,6 @@ public interface ISubscriberService
|
||||
ISubscriber subscriber,
|
||||
CustomerGetOptions customerGetOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the account credit, a masked representation of the default payment source and the tax information for the
|
||||
/// provided <paramref name="subscriber"/>. This is essentially a consolidated invocation of the <see cref="GetPaymentSource"/>
|
||||
/// and <see cref="GetTaxInformation"/> methods with a response that includes the customer's <see cref="Stripe.Customer.Balance"/> as account credit in order to cut down on Stripe API calls.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to retrieve payment method for.</param>
|
||||
/// <returns>A <see cref="Models.PaymentMethod"/> containing the subscriber's account credit, payment source and tax information.</returns>
|
||||
Task<PaymentMethod> GetPaymentMethod(
|
||||
ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a masked representation of the subscriber's payment source for presentation to a client.
|
||||
/// </summary>
|
||||
@@ -107,16 +96,6 @@ public interface ISubscriberService
|
||||
ISubscriber subscriber,
|
||||
SubscriptionGetOptions subscriptionGetOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the <paramref name="subscriber"/>'s tax information using their Stripe <see cref="Stripe.Customer"/>'s <see cref="Stripe.Customer.Address"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to retrieve the tax information for.</param>
|
||||
/// <returns>A <see cref="TaxInformation"/> representing the <paramref name="subscriber"/>'s tax information.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
||||
/// <remarks>This method opts for returning <see langword="null"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>
|
||||
Task<TaxInformation> GetTaxInformation(
|
||||
ISubscriber subscriber);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove a subscriber's saved payment source. If the Stripe <see cref="Stripe.Customer"/> representing the
|
||||
/// <paramref name="subscriber"/> contains a valid <b>"btCustomerId"</b> key in its <see cref="Stripe.Customer.Metadata"/> property,
|
||||
@@ -147,17 +126,6 @@ public interface ISubscriberService
|
||||
ISubscriber subscriber,
|
||||
TaxInformation taxInformation);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the subscriber's pending bank account using the provided <paramref name="descriptorCode"/>.
|
||||
/// </summary>
|
||||
/// <param name="subscriber">The subscriber to verify the bank account for.</param>
|
||||
/// <param name="descriptorCode">The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it.
|
||||
/// <a href="https://docs.stripe.com/payments/ach-debit/set-up-payment">Learn more.</a></param>
|
||||
/// <returns></returns>
|
||||
Task VerifyBankAccount(
|
||||
ISubscriber subscriber,
|
||||
string descriptorCode);
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> exists in the gateway.
|
||||
/// If the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> is <see langword="null"/> or empty, returns <see langword="true"/>.
|
||||
|
||||
@@ -24,7 +24,6 @@ using Stripe;
|
||||
|
||||
using static Bit.Core.Billing.Utilities;
|
||||
using Customer = Stripe.Customer;
|
||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Implementations;
|
||||
@@ -330,38 +329,6 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PaymentMethod> GetPaymentMethod(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
|
||||
var customer = await GetCustomer(subscriber, new CustomerGetOptions
|
||||
{
|
||||
Expand = ["default_source", "invoice_settings.default_payment_method", "subscriptions", "tax_ids"]
|
||||
});
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
return PaymentMethod.Empty;
|
||||
}
|
||||
|
||||
var accountCredit = customer.Balance * -1 / 100M;
|
||||
|
||||
var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer);
|
||||
|
||||
var subscriptionStatus = customer.Subscriptions
|
||||
.FirstOrDefault(subscription => subscription.Id == subscriber.GatewaySubscriptionId)?
|
||||
.Status;
|
||||
|
||||
var taxInformation = GetTaxInformation(customer);
|
||||
|
||||
return new PaymentMethod(
|
||||
accountCredit,
|
||||
paymentMethod,
|
||||
subscriptionStatus,
|
||||
taxInformation);
|
||||
}
|
||||
|
||||
public async Task<PaymentSource> GetPaymentSource(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
@@ -449,16 +416,6 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TaxInformation> GetTaxInformation(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
|
||||
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||
|
||||
return GetTaxInformation(customer);
|
||||
}
|
||||
|
||||
public async Task RemovePaymentSource(
|
||||
ISubscriber subscriber)
|
||||
{
|
||||
@@ -823,57 +780,6 @@ public class SubscriberService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task VerifyBankAccount(
|
||||
ISubscriber subscriber,
|
||||
string descriptorCode)
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id);
|
||||
|
||||
if (string.IsNullOrEmpty(setupIntentId))
|
||||
{
|
||||
logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId,
|
||||
new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode });
|
||||
|
||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
|
||||
|
||||
await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
||||
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
||||
|
||||
await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = setupIntent.PaymentMethodId
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (StripeException stripeException)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(stripeException.StripeError?.Code))
|
||||
{
|
||||
var message = stripeException.StripeError.Code switch
|
||||
{
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.",
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.",
|
||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.",
|
||||
_ => BillingException.DefaultMessage
|
||||
};
|
||||
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
|
||||
logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id);
|
||||
throw new BillingException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(subscriber);
|
||||
@@ -970,25 +876,6 @@ public class SubscriberService(
|
||||
return PaymentSource.From(setupIntent);
|
||||
}
|
||||
|
||||
private static TaxInformation GetTaxInformation(
|
||||
Customer customer)
|
||||
{
|
||||
if (customer.Address == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TaxInformation(
|
||||
customer.Address.Country,
|
||||
customer.Address.PostalCode,
|
||||
customer.TaxIds?.FirstOrDefault()?.Value,
|
||||
customer.TaxIds?.FirstOrDefault()?.Type,
|
||||
customer.Address.Line1,
|
||||
customer.Address.Line2,
|
||||
customer.Address.City,
|
||||
customer.Address.State);
|
||||
}
|
||||
|
||||
private async Task RemoveBraintreeCustomerIdAsync(
|
||||
Customer customer)
|
||||
{
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.StaticStore;
|
||||
@@ -44,8 +42,6 @@ public interface IPaymentService
|
||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||
Task<BillingHistoryInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
||||
Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber);
|
||||
Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber);
|
||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats, int additionalServiceAccount);
|
||||
/// <summary>
|
||||
/// Secrets Manager Standalone is a discount in Stripe that is used to give an organization access to Secrets Manager.
|
||||
@@ -68,7 +64,4 @@ public interface IPaymentService
|
||||
/// <param name="organization">Organization Representation used for Inviting Organization Users</param>
|
||||
/// <returns>If the organization has Secrets Manager and has the Standalone Stripe Discount</returns>
|
||||
Task<bool> HasSecretsManagerStandalone(InviteOrganization organization);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
|
||||
}
|
||||
|
||||
@@ -8,11 +8,7 @@ using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -36,8 +32,6 @@ public class StripePaymentService : IPaymentService
|
||||
private readonly Braintree.IBraintreeGateway _btGateway;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITaxService _taxService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
|
||||
public StripePaymentService(
|
||||
@@ -46,8 +40,6 @@ public class StripePaymentService : IPaymentService
|
||||
IStripeAdapter stripeAdapter,
|
||||
Braintree.IBraintreeGateway braintreeGateway,
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ITaxService taxService,
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_transactionRepository = transactionRepository;
|
||||
@@ -55,8 +47,6 @@ public class StripePaymentService : IPaymentService
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_btGateway = braintreeGateway;
|
||||
_globalSettings = globalSettings;
|
||||
_featureService = featureService;
|
||||
_taxService = taxService;
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
@@ -705,133 +695,6 @@ public class StripePaymentService : IPaymentService
|
||||
return subscriptionInfo;
|
||||
}
|
||||
|
||||
public async Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber)
|
||||
{
|
||||
if (subscriber == null || string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
|
||||
new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var address = customer.Address;
|
||||
var taxId = customer.TaxIds?.FirstOrDefault();
|
||||
|
||||
// Line1 is required, so if missing we're using the subscriber name,
|
||||
// see: https://stripe.com/docs/api/customers/create#create_customer-address-line1
|
||||
if (address != null && string.IsNullOrWhiteSpace(address.Line1))
|
||||
{
|
||||
address.Line1 = null;
|
||||
}
|
||||
|
||||
return new TaxInfo
|
||||
{
|
||||
TaxIdNumber = taxId?.Value,
|
||||
TaxIdType = taxId?.Type,
|
||||
BillingAddressLine1 = address?.Line1,
|
||||
BillingAddressLine2 = address?.Line2,
|
||||
BillingAddressCity = address?.City,
|
||||
BillingAddressState = address?.State,
|
||||
BillingAddressPostalCode = address?.PostalCode,
|
||||
BillingAddressCountry = address?.Country,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subscriber?.GatewayCustomerId) || subscriber.IsUser())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
},
|
||||
Expand = ["tax_ids"]
|
||||
});
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var taxId = customer.TaxIds?.FirstOrDefault();
|
||||
|
||||
if (taxId != null)
|
||||
{
|
||||
await _stripeAdapter.TaxIdDeleteAsync(customer.Id, taxId.Id);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var taxIdType = taxInfo.TaxIdType;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(taxIdType))
|
||||
{
|
||||
taxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber);
|
||||
|
||||
if (taxIdType == null)
|
||||
{
|
||||
_logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||
new TaxIdCreateOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber });
|
||||
|
||||
if (taxInfo.TaxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||
{
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id,
|
||||
new TaxIdCreateOptions
|
||||
{
|
||||
Type = StripeConstants.TaxIdType.EUVAT,
|
||||
Value = $"ES{taxInfo.TaxIdNumber}"
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (StripeException e)
|
||||
{
|
||||
switch (e.StripeError.Code)
|
||||
{
|
||||
case StripeConstants.ErrorCodes.TaxIdInvalid:
|
||||
_logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||
taxInfo.TaxIdNumber,
|
||||
taxInfo.BillingAddressCountry);
|
||||
throw new BadRequestException("billingInvalidTaxIdError");
|
||||
default:
|
||||
_logger.LogError(e,
|
||||
"Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.",
|
||||
taxInfo.TaxIdNumber,
|
||||
taxInfo.BillingAddressCountry,
|
||||
customer.Id);
|
||||
throw new BadRequestException("billingTaxIdCreationError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> AddSecretsManagerToSubscription(
|
||||
Organization org,
|
||||
StaticStore.Plan plan,
|
||||
@@ -909,309 +772,6 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete($"Use {nameof(PreviewPremiumTaxCommand)} instead.")]
|
||||
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||
PreviewIndividualInvoiceRequestBody parameters,
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true, },
|
||||
Currency = "usd",
|
||||
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = 1,
|
||||
Plan = premiumPlan.Seat.StripePriceId
|
||||
},
|
||||
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = premiumPlan.Storage.StripePriceId
|
||||
}
|
||||
]
|
||||
},
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
PostalCode = parameters.TaxInformation.PostalCode,
|
||||
Country = parameters.TaxInformation.Country,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(parameters.TaxInformation.TaxId))
|
||||
{
|
||||
var taxIdType = _taxService.GetStripeTaxCode(
|
||||
options.CustomerDetails.Address.Country,
|
||||
parameters.TaxInformation.TaxId);
|
||||
|
||||
if (taxIdType == null)
|
||||
{
|
||||
_logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingPreviewInvalidTaxIdError");
|
||||
}
|
||||
|
||||
options.CustomerDetails.TaxIds =
|
||||
[
|
||||
new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId }
|
||||
];
|
||||
|
||||
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||
{
|
||||
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
|
||||
{
|
||||
Type = StripeConstants.TaxIdType.EUVAT,
|
||||
Value = $"ES{parameters.TaxInformation.TaxId}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
|
||||
{
|
||||
var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
|
||||
|
||||
if (gatewayCustomer.Discount != null)
|
||||
{
|
||||
options.Discounts = [new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id }];
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId))
|
||||
{
|
||||
var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
|
||||
|
||||
if (gatewaySubscription?.Discounts is { Count: > 0 })
|
||||
{
|
||||
options.Discounts = gatewaySubscription.Discounts.Select(x => new InvoiceDiscountOptions { Coupon = x.Coupon.Id }).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.Discounts is { Count: > 0 })
|
||||
{
|
||||
options.Discounts = options.Discounts.DistinctBy(invoiceDiscountOptions => invoiceDiscountOptions.Coupon).ToList();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
|
||||
var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount);
|
||||
|
||||
var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0
|
||||
? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
||||
: 0M;
|
||||
|
||||
var result = new PreviewInvoiceResponseModel(
|
||||
effectiveTaxRate,
|
||||
invoice.TotalExcludingTax.ToMajor() ?? 0,
|
||||
tax.ToMajor(),
|
||||
invoice.Total.ToMajor());
|
||||
return result;
|
||||
}
|
||||
catch (StripeException e)
|
||||
{
|
||||
switch (e.StripeError.Code)
|
||||
{
|
||||
case StripeConstants.ErrorCodes.TaxIdInvalid:
|
||||
_logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingPreviewInvalidTaxIdError");
|
||||
default:
|
||||
_logger.LogError(e,
|
||||
"Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingPreviewInvoiceError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||
PreviewOrganizationInvoiceRequestBody parameters,
|
||||
string gatewayCustomerId,
|
||||
string gatewaySubscriptionId)
|
||||
{
|
||||
var plan = await _pricingClient.GetPlanOrThrow(parameters.PasswordManager.Plan);
|
||||
var isSponsored = parameters.PasswordManager.SponsoredPlan.HasValue;
|
||||
|
||||
var options = new InvoiceCreatePreviewOptions
|
||||
{
|
||||
Currency = "usd",
|
||||
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||
Plan = plan.PasswordManager.StripeStoragePlanId
|
||||
}
|
||||
]
|
||||
},
|
||||
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||
{
|
||||
Address = new AddressOptions
|
||||
{
|
||||
PostalCode = parameters.TaxInformation.PostalCode,
|
||||
Country = parameters.TaxInformation.Country,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (isSponsored)
|
||||
{
|
||||
var sponsoredPlan = SponsoredPlans.Get(parameters.PasswordManager.SponsoredPlan.Value);
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = sponsoredPlan.StripePlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (plan.PasswordManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = parameters.PasswordManager.Seats, Plan = plan.PasswordManager.StripeSeatPlanId }
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(
|
||||
new InvoiceSubscriptionDetailsItemOptions { Quantity = 1, Plan = plan.PasswordManager.StripePlanId }
|
||||
);
|
||||
}
|
||||
|
||||
if (plan.SupportsSecretsManager)
|
||||
{
|
||||
if (plan.SecretsManager.HasAdditionalSeatsOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||
});
|
||||
}
|
||||
|
||||
if (plan.SecretsManager.HasAdditionalServiceAccountOption)
|
||||
{
|
||||
options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions
|
||||
{
|
||||
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parameters.TaxInformation.TaxId))
|
||||
{
|
||||
var taxIdType = _taxService.GetStripeTaxCode(
|
||||
options.CustomerDetails.Address.Country,
|
||||
parameters.TaxInformation.TaxId);
|
||||
|
||||
if (taxIdType == null)
|
||||
{
|
||||
_logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
|
||||
options.CustomerDetails.TaxIds =
|
||||
[
|
||||
new InvoiceCustomerDetailsTaxIdOptions { Type = taxIdType, Value = parameters.TaxInformation.TaxId }
|
||||
];
|
||||
|
||||
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
|
||||
{
|
||||
options.CustomerDetails.TaxIds.Add(new InvoiceCustomerDetailsTaxIdOptions
|
||||
{
|
||||
Type = StripeConstants.TaxIdType.EUVAT,
|
||||
Value = $"ES{parameters.TaxInformation.TaxId}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Customer gatewayCustomer = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
|
||||
{
|
||||
gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
|
||||
|
||||
if (gatewayCustomer.Discount != null)
|
||||
{
|
||||
options.Discounts =
|
||||
[
|
||||
new InvoiceDiscountOptions { Coupon = gatewayCustomer.Discount.Coupon.Id }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gatewaySubscriptionId))
|
||||
{
|
||||
var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
|
||||
|
||||
if (gatewaySubscription?.Discounts != null)
|
||||
{
|
||||
options.Discounts = gatewaySubscription.Discounts
|
||||
.Select(discount => new InvoiceDiscountOptions { Coupon = discount.Coupon.Id }).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
if (parameters.PasswordManager.Plan.IsBusinessProductTierType() &&
|
||||
parameters.TaxInformation.Country != Constants.CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
|
||||
var tax = invoice.TotalTaxes.Sum(invoiceTotalTax => invoiceTotalTax.Amount);
|
||||
|
||||
var effectiveTaxRate = invoice.TotalExcludingTax != null && invoice.TotalExcludingTax.Value != 0
|
||||
? tax.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
||||
: 0M;
|
||||
|
||||
var result = new PreviewInvoiceResponseModel(
|
||||
effectiveTaxRate,
|
||||
invoice.TotalExcludingTax.ToMajor() ?? 0,
|
||||
tax.ToMajor(),
|
||||
invoice.Total.ToMajor());
|
||||
return result;
|
||||
}
|
||||
catch (StripeException e)
|
||||
{
|
||||
switch (e.StripeError.Code)
|
||||
{
|
||||
case StripeConstants.ErrorCodes.TaxIdInvalid:
|
||||
_logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingPreviewInvalidTaxIdError");
|
||||
default:
|
||||
_logger.LogError(e,
|
||||
"Unexpected error previewing invoice with tax ID '{TaxId}' in country '{Country}'.",
|
||||
parameters.TaxInformation.TaxId,
|
||||
parameters.TaxInformation.Country);
|
||||
throw new BadRequestException("billingPreviewInvoiceError");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
{
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||
|
||||
Reference in New Issue
Block a user