mirror of
https://github.com/bitwarden/server.git
synced 2024-11-29 13:25:17 +01:00
wip
This commit is contained in:
parent
7fc9d51220
commit
ce85c38aa7
59
src/Api/Billing/Controllers/InvoicesController.cs
Normal file
59
src/Api/Billing/Controllers/InvoicesController.cs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Controllers;
|
||||||
|
|
||||||
|
[Route("invoices")]
|
||||||
|
[Authorize("Application")]
|
||||||
|
public class InvoicesController : BaseBillingController
|
||||||
|
{
|
||||||
|
[HttpPost("preview-organization")]
|
||||||
|
public async Task<IResult> PreviewInvoiceAsync(
|
||||||
|
[FromBody] PreviewOrganizationInvoiceRequestBody model,
|
||||||
|
[FromServices] ICurrentContext currentContext,
|
||||||
|
[FromServices] IOrganizationRepository organizationRepository,
|
||||||
|
[FromServices] IPaymentService paymentService)
|
||||||
|
{
|
||||||
|
Organization? organization = null;
|
||||||
|
if (model.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
if (!await currentContext.EditPaymentMethods(model.OrganizationId.Value))
|
||||||
|
{
|
||||||
|
return Error.Unauthorized();
|
||||||
|
}
|
||||||
|
organization = await organizationRepository.GetByIdAsync(model.OrganizationId.Value);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return Error.NotFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId, organization?.GatewaySubscriptionId);
|
||||||
|
|
||||||
|
return TypedResults.Ok(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -311,16 +311,17 @@ public class OrganizationsController(
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var taxInfo = new TaxInformation(
|
var taxInfo = new TaxInfo
|
||||||
model.Country,
|
{
|
||||||
model.PostalCode,
|
TaxIdNumber = model.TaxId,
|
||||||
model.TaxId,
|
BillingAddressLine1 = model.Line1,
|
||||||
model.TaxIdType,
|
BillingAddressLine2 = model.Line2,
|
||||||
model.Line1,
|
BillingAddressCity = model.City,
|
||||||
model.Line2,
|
BillingAddressState = model.State,
|
||||||
model.City,
|
BillingAddressPostalCode = model.PostalCode,
|
||||||
model.State);
|
BillingAddressCountry = model.Country,
|
||||||
await subscriberService.UpdateTaxInformation(organization, taxInfo);
|
};
|
||||||
|
await paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
|
|
||||||
|
public class PreviewOrganizationInvoiceRequestBody
|
||||||
|
{
|
||||||
|
public Guid? OrganizationId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public PasswordManagerRequestModel PasswordManager { get; set; }
|
||||||
|
|
||||||
|
public SecretsManagerRequestModel? SecretsManager { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public TaxInformationRequestModel TaxInformation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordManagerRequestModel
|
||||||
|
{
|
||||||
|
public PlanType Plan { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int Seats { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int AdditionalStorage { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecretsManagerRequestModel
|
||||||
|
{
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int Seats { get; set; }
|
||||||
|
|
||||||
|
[Range(0, int.MaxValue)]
|
||||||
|
public int AdditionalMachineAccounts { get; set; }
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
using Bit.Core.Billing.Models.Api.Responses;
|
using Bit.Core.Billing.Models.Api.Responses;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -62,4 +63,6 @@ public interface IPaymentService
|
|||||||
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
||||||
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
|
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
|
||||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||||
|
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
using Bit.Core.Billing.Models.Api.Requests.Accounts;
|
||||||
|
using Bit.Core.Billing.Models.Api.Requests.Organizations;
|
||||||
using Bit.Core.Billing.Models.Api.Responses;
|
using Bit.Core.Billing.Models.Api.Responses;
|
||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
@ -2001,6 +2003,161 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(
|
||||||
|
PreviewOrganizationInvoiceRequestBody parameters,
|
||||||
|
string gatewayCustomerId,
|
||||||
|
string gatewaySubscriptionId)
|
||||||
|
{
|
||||||
|
var plan = Utilities.StaticStore.GetPlan(parameters.PasswordManager.Plan);
|
||||||
|
|
||||||
|
var options = new InvoiceCreatePreviewOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new InvoiceAutomaticTaxOptions
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
},
|
||||||
|
Currency = "usd",
|
||||||
|
Discounts = new List<InvoiceDiscountOptions>(),
|
||||||
|
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
|
||||||
|
{
|
||||||
|
Items =
|
||||||
|
[
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Quantity = parameters.PasswordManager.AdditionalStorage,
|
||||||
|
Plan = plan.PasswordManager.StripeStoragePlanId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
CustomerDetails = new InvoiceCustomerDetailsOptions
|
||||||
|
{
|
||||||
|
Address = new AddressOptions
|
||||||
|
{
|
||||||
|
PostalCode = parameters.TaxInformation.PostalCode,
|
||||||
|
Country = parameters.TaxInformation.Country,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (plan.ProductTier)
|
||||||
|
{
|
||||||
|
case ProductTierType.Families:
|
||||||
|
options.SubscriptionDetails.Items.Add(
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Quantity = 1,
|
||||||
|
Plan = plan.PasswordManager.StripePlanId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case ProductTierType.Teams:
|
||||||
|
case ProductTierType.TeamsStarter:
|
||||||
|
case ProductTierType.Enterprise:
|
||||||
|
options.SubscriptionDetails.Items.Add(
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Quantity = parameters.PasswordManager.Seats,
|
||||||
|
Plan = plan.PasswordManager.StripeSeatPlanId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plan.SupportsSecretsManager)
|
||||||
|
{
|
||||||
|
options.SubscriptionDetails.Items.AddRange([
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Quantity = parameters.SecretsManager?.Seats ?? 0,
|
||||||
|
Plan = plan.SecretsManager.StripeSeatPlanId
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Quantity = parameters.SecretsManager?.AdditionalMachineAccounts ?? 0,
|
||||||
|
Plan = plan.SecretsManager.StripeServiceAccountPlanId
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (gatewayCustomerId != null)
|
||||||
|
{
|
||||||
|
var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
|
||||||
|
|
||||||
|
if (gatewayCustomer.Discount != null)
|
||||||
|
{
|
||||||
|
options.Discounts.Add(new InvoiceDiscountOptions
|
||||||
|
{
|
||||||
|
Discount = gatewayCustomer.Discount.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var gatewaySubscription = await _stripeAdapter.SubscriptionGetAsync(gatewaySubscriptionId);
|
||||||
|
|
||||||
|
if (gatewaySubscription?.Discount != null)
|
||||||
|
{
|
||||||
|
options.Discounts.Add(new InvoiceDiscountOptions
|
||||||
|
{
|
||||||
|
Discount = gatewaySubscription.Discount.Id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||||
|
|
||||||
|
var effectiveTaxRate = invoice.Tax != null && invoice.TotalExcludingTax != null
|
||||||
|
? invoice.Tax.Value.ToMajor() / invoice.TotalExcludingTax.Value.ToMajor()
|
||||||
|
: 0M;
|
||||||
|
|
||||||
|
var result = new PreviewInvoiceResponseModel(
|
||||||
|
effectiveTaxRate,
|
||||||
|
invoice.TotalExcludingTax.ToMajor() ?? 0,
|
||||||
|
invoice.Tax.ToMajor() ?? 0,
|
||||||
|
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)
|
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||||
{
|
{
|
||||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||||
|
Loading…
Reference in New Issue
Block a user