1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00
This commit is contained in:
Jonas Hendrickx 2024-11-25 09:34:20 +01:00
parent 7fc9d51220
commit ce85c38aa7
5 changed files with 267 additions and 10 deletions

View 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);
}
}

View File

@ -311,16 +311,17 @@ public class OrganizationsController(
throw new NotFoundException();
}
var taxInfo = new TaxInformation(
model.Country,
model.PostalCode,
model.TaxId,
model.TaxIdType,
model.Line1,
model.Line2,
model.City,
model.State);
await subscriberService.UpdateTaxInformation(organization, taxInfo);
var taxInfo = new TaxInfo
{
TaxIdNumber = model.TaxId,
BillingAddressLine1 = model.Line1,
BillingAddressLine2 = model.Line2,
BillingAddressCity = model.City,
BillingAddressState = model.State,
BillingAddressPostalCode = model.PostalCode,
BillingAddressCountry = model.Country,
};
await paymentService.SaveTaxInfoAsync(organization, taxInfo);
}
/// <summary>

View File

@ -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; }
}

View File

@ -2,6 +2,7 @@
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.Requests.Organizations;
using Bit.Core.Billing.Models.Api.Responses;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -62,4 +63,6 @@ public interface IPaymentService
Task<bool> HasSecretsManagerStandalone(Organization organization);
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
}

View File

@ -1,9 +1,11 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
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.Business;
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)
{
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(