mirror of
https://github.com/bitwarden/server.git
synced 2024-11-23 12:25:16 +01:00
wip
This commit is contained in:
parent
8fedd646c3
commit
efd4dab069
@ -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);
|
||||
}
|
||||
}
|
||||
|
33
src/Core/Billing/Extensions/CurrencyExtensions.cs
Normal file
33
src/Core/Billing/Extensions/CurrencyExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Billing.Models.Api.Responses;
|
||||
|
||||
public record PreviewInvoiceResponseModel(
|
||||
decimal EffectiveTaxRate,
|
||||
decimal TaxableBaseAmount,
|
||||
decimal TaxAmount,
|
||||
decimal TotalAmount);
|
7
src/Core/Billing/Models/PreviewInvoiceInfo.cs
Normal file
7
src/Core/Billing/Models/PreviewInvoiceInfo.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record PreviewInvoiceInfo(
|
||||
decimal EffectiveTaxRate,
|
||||
decimal TaxableBaseAmount,
|
||||
decimal TaxAmount,
|
||||
decimal TotalAmount);
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
Loading…
Reference in New Issue
Block a user