1
0
mirror of https://github.com/bitwarden/server synced 2025-12-30 15:14:02 +00:00

[PM-25463] Work towards complete usage of Payments domain (#6363)

* Use payment domain

* Run dotnet format and remove unused code

* Fix swagger

* Stephon's feedback

* Run dotnet format
This commit is contained in:
Alex Morask
2025-10-01 10:26:39 -05:00
committed by GitHub
parent 7cefca330b
commit 61265c7533
46 changed files with 2988 additions and 1350 deletions

View File

@@ -1,16 +1,8 @@
#nullable enable
using System.Diagnostics;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -28,10 +20,8 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IPricingClient pricingClient,
ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController
IPaymentHistoryService paymentHistoryService) : BaseBillingController
{
[HttpGet("metadata")]
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
@@ -264,71 +254,6 @@ public class OrganizationBillingController(
return TypedResults.Ok();
}
[HttpPost("restart-subscription")]
public async Task<IResult> RestartSubscriptionAsync([FromRoute] Guid organizationId,
[FromBody] OrganizationCreateRequestModel model)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await currentContext.EditPaymentMethods(organizationId))
{
return Error.Unauthorized();
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
return Error.NotFound();
}
var existingPlan = organization.PlanType;
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true;
if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
{
sale.Organization.UseTotp = plan.HasTotp;
sale.Organization.UseGroups = plan.HasGroups;
sale.Organization.UseDirectory = plan.HasDirectory;
sale.Organization.SelfHost = plan.HasSelfHost;
sale.Organization.UsersGetPremium = plan.UsersGetPremium;
sale.Organization.UseEvents = plan.HasEvents;
sale.Organization.Use2fa = plan.Has2fa;
sale.Organization.UseApi = plan.HasApi;
sale.Organization.UsePolicies = plan.HasPolicies;
sale.Organization.UseSso = plan.HasSso;
sale.Organization.UseResetPassword = plan.HasResetPassword;
sale.Organization.UseKeyConnector = plan.HasKeyConnector ? organization.UseKeyConnector : false;
sale.Organization.UseScim = plan.HasScim;
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
}
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
{
return Error.BadRequest("A payment method is required to restart the subscription.");
}
var org = await organizationRepository.GetByIdAsync(organizationId);
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
await organizationBillingService.Finalize(sale);
var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
if (updatedOrg != null)
{
await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
}
return TypedResults.Ok();
}
[HttpPost("setup-business-unit")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IResult> SetupBusinessUnitAsync(

View File

@@ -1,33 +1,73 @@
using Bit.Api.Billing.Models.Requests;
using Bit.Core.Billing.Tax.Commands;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Tax;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Organizations.Commands;
using Bit.Core.Billing.Premium.Commands;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
[Route("tax")]
[Route("billing/tax")]
public class TaxController(
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
IPreviewOrganizationTaxCommand previewOrganizationTaxCommand,
IPreviewPremiumTaxCommand previewPremiumTaxCommand) : BaseBillingController
{
[HttpPost("preview-amount/organization-trial")]
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
[HttpPost("organizations/subscriptions/purchase")]
public async Task<IResult> PreviewOrganizationSubscriptionPurchaseTaxAsync(
[FromBody] PreviewOrganizationSubscriptionPurchaseTaxRequest request)
{
var parameters = new OrganizationTrialParameters
var (purchase, billingAddress) = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(purchase, billingAddress);
return Handle(result.Map(pair => new
{
PlanType = requestBody.PlanType,
ProductType = requestBody.ProductType,
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
{
Country = requestBody.TaxInformation.Country,
PostalCode = requestBody.TaxInformation.PostalCode,
TaxId = requestBody.TaxInformation.TaxId
}
};
pair.Tax,
pair.Total
}));
}
var result = await previewTaxAmountCommand.Run(parameters);
[HttpPost("organizations/{organizationId:guid}/subscription/plan-change")]
[InjectOrganization]
public async Task<IResult> PreviewOrganizationSubscriptionPlanChangeTaxAsync(
[BindNever] Organization organization,
[FromBody] PreviewOrganizationSubscriptionPlanChangeTaxRequest request)
{
var (planChange, billingAddress) = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
return Handle(result);
[HttpPut("organizations/{organizationId:guid}/subscription/update")]
[InjectOrganization]
public async Task<IResult> PreviewOrganizationSubscriptionUpdateTaxAsync(
[BindNever] Organization organization,
[FromBody] PreviewOrganizationSubscriptionUpdateTaxRequest request)
{
var update = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(organization, update);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
[HttpPost("premium/subscriptions/purchase")]
public async Task<IResult> PreviewPremiumSubscriptionPurchaseTaxAsync(
[FromBody] PreviewPremiumSubscriptionPurchaseTaxRequest request)
{
var (purchase, billingAddress) = request.ToDomain();
var result = await previewPremiumTaxCommand.Run(purchase, billingAddress);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
}

View File

@@ -1,5 +1,4 @@
#nullable enable
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Premium;
using Bit.Core;

View File

@@ -2,11 +2,14 @@
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Subscriptions;
using Bit.Api.Billing.Models.Requirements;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -24,6 +27,7 @@ public class OrganizationBillingVNextController(
IGetCreditQuery getCreditQuery,
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IRestartSubscriptionCommand restartSubscriptionCommand,
IUpdateBillingAddressCommand updateBillingAddressCommand,
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
{
@@ -95,6 +99,20 @@ public class OrganizationBillingVNextController(
return Handle(result);
}
[Authorize<ManageOrganizationBillingRequirement>]
[HttpPost("subscription/restart")]
[InjectOrganization]
public async Task<IResult> RestartSubscriptionAsync(
[BindNever] Organization organization,
[FromBody] RestartSubscriptionRequest request)
{
var (paymentMethod, billingAddress) = request.ToDomain();
var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, null)
.AndThenAsync(_ => updateBillingAddressCommand.Run(organization, billingAddress))
.AndThenAsync(_ => restartSubscriptionCommand.Run(organization));
return Handle(result);
}
[Authorize<MemberOrProviderRequirement>]
[HttpGet("warnings")]
[InjectOrganization]