mirror of
https://github.com/bitwarden/server
synced 2025-12-10 21:33:41 +00:00
MOAR DELETE
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class TaxInformationRequestBody
|
||||
{
|
||||
[Required]
|
||||
public string Country { get; set; }
|
||||
[Required]
|
||||
public string PostalCode { get; set; }
|
||||
public string TaxId { 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 TaxInformation ToDomain() => new(
|
||||
Country,
|
||||
PostalCode,
|
||||
TaxId,
|
||||
TaxIdType,
|
||||
Line1,
|
||||
Line2,
|
||||
City,
|
||||
State);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class TokenizedPaymentSourceRequestBody
|
||||
{
|
||||
[Required]
|
||||
[EnumMatches<PaymentMethodType>(
|
||||
PaymentMethodType.BankAccount,
|
||||
PaymentMethodType.Card,
|
||||
PaymentMethodType.PayPal,
|
||||
ErrorMessage = "'type' must be BankAccount, Card or PayPal")]
|
||||
public PaymentMethodType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
|
||||
public TokenizedPaymentSource ToDomain() => new(Type, Token);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class UpdatePaymentMethodRequestBody
|
||||
{
|
||||
[Required]
|
||||
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
|
||||
|
||||
[Required]
|
||||
public TaxInformationRequestBody TaxInformation { get; set; }
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class VerifyBankAccountRequestBody
|
||||
{
|
||||
[Required]
|
||||
public string DescriptorCode { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public class BillingPaymentResponseModel : ResponseModel
|
||||
{
|
||||
public BillingPaymentResponseModel(BillingInfo billing)
|
||||
: base("billingPayment")
|
||||
{
|
||||
Balance = billing.Balance;
|
||||
PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null;
|
||||
}
|
||||
|
||||
public decimal Balance { get; set; }
|
||||
public BillingSource PaymentSource { get; set; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentMethodResponse(
|
||||
decimal AccountCredit,
|
||||
PaymentSource PaymentSource,
|
||||
string SubscriptionStatus,
|
||||
TaxInformation TaxInformation)
|
||||
{
|
||||
public static PaymentMethodResponse From(PaymentMethod paymentMethod) =>
|
||||
new(
|
||||
paymentMethod.AccountCredit,
|
||||
paymentMethod.PaymentSource,
|
||||
paymentMethod.SubscriptionStatus,
|
||||
paymentMethod.TaxInformation);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentSourceResponse(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static PaymentSourceResponse From(PaymentSource paymentMethod)
|
||||
=> new(
|
||||
paymentMethod.Type,
|
||||
paymentMethod.Description,
|
||||
paymentMethod.NeedsVerification);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record TaxInformationResponse(
|
||||
string Country,
|
||||
string PostalCode,
|
||||
string TaxId,
|
||||
string Line1,
|
||||
string Line2,
|
||||
string City,
|
||||
string State)
|
||||
{
|
||||
public static TaxInformationResponse From(TaxInformation taxInformation)
|
||||
=> new(
|
||||
taxInformation.Country,
|
||||
taxInformation.PostalCode,
|
||||
taxInformation.TaxId,
|
||||
taxInformation.Line1,
|
||||
taxInformation.Line2,
|
||||
taxInformation.City,
|
||||
taxInformation.State);
|
||||
}
|
||||
@@ -44,8 +44,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 +66,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(
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.Billing.Mocks.Plans;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
@@ -17,506 +13,6 @@ namespace Bit.Core.Test.Services;
|
||||
[SutProviderCustomize]
|
||||
public class StripePaymentServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager =
|
||||
new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
AdditionalStorage = 0
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 0)))
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 4000,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 800 }],
|
||||
Total = 4800
|
||||
});
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(8M, actual.TaxAmount);
|
||||
Assert.Equal(48M, actual.TotalAmount);
|
||||
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager =
|
||||
new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
AdditionalStorage = 1
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripePlanId &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 1)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 4000, TotalTaxes = [new InvoiceTotalTax { Amount = 800 }], Total = 4800 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(8M, actual.TaxAmount);
|
||||
Assert.Equal(48M, actual.TotalAmount);
|
||||
Assert.Equal(40M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
AdditionalStorage = 0
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 0)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 0, TotalTaxes = [new InvoiceTotalTax { Amount = 0 }], Total = 0 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(0M, actual.TaxAmount);
|
||||
Assert.Equal(0M, actual.TotalAmount);
|
||||
Assert.Equal(0M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
|
||||
SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually,
|
||||
SponsoredPlan = PlanSponsorshipType.FamiliesForEnterprise,
|
||||
AdditionalStorage = 1
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel { Country = "FR", PostalCode = "12345" }
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>()
|
||||
.InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(p =>
|
||||
p.Currency == "usd" &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == "2021-family-for-enterprise-annually" &&
|
||||
x.Quantity == 1) &&
|
||||
p.SubscriptionDetails.Items.Any(x =>
|
||||
x.Plan == familiesPlan.PasswordManager.StripeStoragePlanId &&
|
||||
x.Quantity == 1)))
|
||||
.Returns(new Invoice { TotalExcludingTax = 400, TotalTaxes = [new InvoiceTotalTax { Amount = 8 }], Total = 408 });
|
||||
|
||||
var actual = await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
Assert.Equal(0.08M, actual.TaxAmount);
|
||||
Assert.Equal(4.08M, actual.TotalAmount);
|
||||
Assert.Equal(4M, actual.TaxableBaseAmount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.AutomaticTax.Enabled == true
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "US",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var familiesPlan = new FamiliesPlan();
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
|
||||
.Returns(familiesPlan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.FamiliesAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == null
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider<StripePaymentService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var plan = new EnterprisePlan(true);
|
||||
sutProvider.GetDependency<IPricingClient>()
|
||||
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
|
||||
.Returns(plan);
|
||||
|
||||
var parameters = new PreviewOrganizationInvoiceRequestBody
|
||||
{
|
||||
PasswordManager = new OrganizationPasswordManagerRequestModel
|
||||
{
|
||||
Plan = PlanType.EnterpriseAnnually
|
||||
},
|
||||
TaxInformation = new TaxInformationRequestModel
|
||||
{
|
||||
Country = "FR",
|
||||
PostalCode = "12345"
|
||||
}
|
||||
};
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter
|
||||
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
|
||||
.Returns(new Invoice
|
||||
{
|
||||
TotalExcludingTax = 400,
|
||||
TotalTaxes = [new InvoiceTotalTax { Amount = 8 }],
|
||||
Total = 408
|
||||
});
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
|
||||
|
||||
// Assert
|
||||
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
|
||||
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetSubscriptionAsync_WithCustomerDiscount_ReturnsDiscountFromCustomer(
|
||||
|
||||
Reference in New Issue
Block a user