1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00
This commit is contained in:
Jonas Hendrickx 2024-11-20 10:33:55 +01:00
parent 8fedd646c3
commit efd4dab069
10 changed files with 188 additions and 0 deletions

View File

@ -1,5 +1,6 @@
#nullable enable
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -77,4 +78,12 @@ public class AccountsBillingController(
return TypedResults.Ok(transactions);
}
[HttpPost("preview-invoice"), AllowAnonymous]
public async Task<IResult> PreviewInvoiceAsync([FromBody] PreviewInvoiceRequestBody model)
{
var invoice = await paymentService.PreviewInvoiceAsync(model);
return TypedResults.Ok(invoice);
}
}

View File

@ -0,0 +1,33 @@
namespace Bit.Core.Billing.Extensions;
public static class CurrencyExtensions
{
/// <summary>
/// Converts a currency amount in major units to minor units.
/// </summary>
/// <example>123.99 USD returns 12399 in minor units.</example>
public static long ToMinor(this decimal amount)
{
return Convert.ToInt64(amount * 100);
}
/// <summary>
/// Converts a currency amount in minor units to major units.
/// </summary>
/// <param name="amount"></param>
/// <example>12399 in minor units returns 123.99 USD.</example>
public static decimal? ToMajor(this long? amount)
{
return amount?.ToMajor();
}
/// <summary>
/// Converts a currency amount in minor units to major units.
/// </summary>
/// <param name="amount"></param>
/// <example>12399 in minor units returns 123.99 USD.</example>
public static decimal ToMajor(this long amount)
{
return Convert.ToDecimal(amount) / 100;
}
}

View File

@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Billing.Models.Api.Requests.Accounts;
public class PreviewInvoiceRequestBody
{
[Required]
public PasswordManagerRequestModel PasswordManager { get; set; }
[Required]
public TaxInformationRequestModel TaxInformation { get; set; }
}
public class PasswordManagerRequestModel
{
[Range(0, int.MaxValue)]
public int AdditionalStorage { get; set; }
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Billing.Models.Api.Requests;
public class TaxInformationRequestModel
{
[Length(2, 2), Required]
public string Country { get; set; }
[Required]
public string PostalCode { get; set; }
public string? TaxId { get; set; }
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Billing.Models.Api.Responses;
public record PreviewInvoiceResponseModel(
decimal EffectiveTaxRate,
decimal TaxableBaseAmount,
decimal TaxAmount,
decimal TotalAmount);

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Billing.Models;
public record PreviewInvoiceInfo(
decimal EffectiveTaxRate,
decimal TaxableBaseAmount,
decimal TaxAmount,
decimal TotalAmount);

View File

@ -1,6 +1,8 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Models.Api.Responses;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
@ -59,4 +61,5 @@ public interface IPaymentService
Task<bool> RisksSubscriptionFailure(Organization organization);
Task<bool> HasSecretsManagerStandalone(Organization organization);
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewInvoiceRequestBody parameters);
}

View File

@ -31,6 +31,7 @@ public interface IStripeAdapter
Task<Stripe.Invoice> InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options);
Task<Stripe.Invoice> InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options);
Task<List<Stripe.Invoice>> InvoiceListAsync(StripeInvoiceListOptions options);
Task<Stripe.Invoice> InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options);
Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options);
Task<Stripe.Invoice> InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options);
Task<Stripe.Invoice> InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options);
@ -42,6 +43,7 @@ public interface IStripeAdapter
IAsyncEnumerable<Stripe.PaymentMethod> PaymentMethodListAutoPagingAsync(Stripe.PaymentMethodListOptions options);
Task<Stripe.PaymentMethod> PaymentMethodAttachAsync(string id, Stripe.PaymentMethodAttachOptions options = null);
Task<Stripe.PaymentMethod> PaymentMethodDetachAsync(string id, Stripe.PaymentMethodDetachOptions options = null);
Task<Stripe.Plan> PlanGetAsync(string id, Stripe.PlanGetOptions options = null);
Task<Stripe.TaxRate> TaxRateCreateAsync(Stripe.TaxRateCreateOptions options);
Task<Stripe.TaxRate> TaxRateUpdateAsync(string id, Stripe.TaxRateUpdateOptions options);
Task<Stripe.TaxId> TaxIdCreateAsync(string id, Stripe.TaxIdCreateOptions options);

View File

@ -15,6 +15,7 @@ public class StripeAdapter : IStripeAdapter
private readonly Stripe.RefundService _refundService;
private readonly Stripe.CardService _cardService;
private readonly Stripe.BankAccountService _bankAccountService;
private readonly Stripe.PlanService _planService;
private readonly Stripe.PriceService _priceService;
private readonly Stripe.SetupIntentService _setupIntentService;
private readonly Stripe.TestHelpers.TestClockService _testClockService;
@ -33,6 +34,7 @@ public class StripeAdapter : IStripeAdapter
_cardService = new Stripe.CardService();
_bankAccountService = new Stripe.BankAccountService();
_priceService = new Stripe.PriceService();
_planService = new Stripe.PlanService();
_setupIntentService = new SetupIntentService();
_testClockService = new Stripe.TestHelpers.TestClockService();
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
@ -133,6 +135,11 @@ public class StripeAdapter : IStripeAdapter
return invoices;
}
public Task<Invoice> InvoiceCreatePreviewAsync(InvoiceCreatePreviewOptions options)
{
return _invoiceService.CreatePreviewAsync(options);
}
public async Task<List<Stripe.Invoice>> InvoiceSearchAsync(InvoiceSearchOptions options)
=> (await _invoiceService.SearchAsync(options)).Data;
@ -184,6 +191,11 @@ public class StripeAdapter : IStripeAdapter
return _paymentMethodService.DetachAsync(id, options);
}
public Task<Stripe.Plan> PlanGetAsync(string id, Stripe.PlanGetOptions options = null)
{
return _planService.GetAsync(id, options);
}
public Task<Stripe.TaxRate> TaxRateCreateAsync(Stripe.TaxRateCreateOptions options)
{
return _taxRateService.CreateAsync(options);

View File

@ -1,7 +1,10 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Api.Requests.Accounts;
using Bit.Core.Billing.Models.Api.Responses;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
@ -1879,6 +1882,86 @@ public class StripePaymentService : IPaymentService
}
}
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewInvoiceRequestBody parameters)
{
var pmStripePlan = await _stripeAdapter.PlanGetAsync("premium-annually");
var storageStripePlan = await _stripeAdapter.PlanGetAsync("storage-gb-annually");
var options = new InvoiceCreatePreviewOptions
{
AutomaticTax = new InvoiceAutomaticTaxOptions
{
Enabled = true,
},
Currency = "usd",
InvoiceItems = new List<InvoiceUpcomingInvoiceItemOptions>
{
new()
{
Quantity = 1,
PriceData = new InvoiceItemPriceDataOptions
{
Currency = "usd",
UnitAmount = pmStripePlan.Amount,
Product = pmStripePlan.ProductId
}
},
new()
{
Quantity = parameters.PasswordManager.AdditionalStorage,
PriceData = new InvoiceItemPriceDataOptions
{
Currency = "usd",
UnitAmount = storageStripePlan.Amount,
Product = storageStripePlan.ProductId
}
}
},
CustomerDetails = new InvoiceCustomerDetailsOptions
{
Address = new AddressOptions
{
PostalCode = parameters.TaxInformation.PostalCode,
Country = parameters.TaxInformation.Country,
}
},
};
if (parameters.TaxInformation.TaxId != null)
{
var taxIdType = _taxService.GetStripeTaxCode(
options.CustomerDetails.Address.Country,
parameters.TaxInformation.TaxId);
if (taxIdType != null)
{
options.CustomerDetails.TaxIds = [
new InvoiceCustomerDetailsTaxIdOptions
{
Type = taxIdType,
Value = parameters.TaxInformation.TaxId
}
];
}
}
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
decimal effectiveTaxRate = 0;
if (invoice.Tax != null && invoice.TotalExcludingTax != null)
{
effectiveTaxRate = invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor();
}
var result = new PreviewInvoiceResponseModel(
effectiveTaxRate,
invoice.TotalExcludingTax.ToMajor() ?? 0,
invoice.Tax.ToMajor() ?? 0,
invoice.Total.ToMajor());
return result;
}
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
{
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(