mirror of
https://github.com/bitwarden/server
synced 2026-02-06 11:44:06 +00:00
* move billing services+tests to billing namespaces * reorganized methods in file and added comment headers * renamed StripeAdapter methods for better clarity * clean up redundant qualifiers * Upgrade Stripe.net to v48.4.0 * Update PreviewTaxAmountCommand * Remove unused UpcomingInvoiceOptionExtensions * Added SubscriptionExtensions with GetCurrentPeriodEnd * Update PremiumUserBillingService * Update OrganizationBillingService * Update GetOrganizationWarningsQuery * Update BillingHistoryInfo * Update SubscriptionInfo * Remove unused Sql Billing folder * Update StripeAdapter * Update StripePaymentService * Update InvoiceCreatedHandler * Update PaymentFailedHandler * Update PaymentSucceededHandler * Update ProviderEventService * Update StripeEventUtilityService * Update SubscriptionDeletedHandler * Update SubscriptionUpdatedHandler * Update UpcomingInvoiceHandler * Update ProviderSubscriptionResponse * Remove unused Stripe Subscriptions Admin Tool * Update RemoveOrganizationFromProviderCommand * Update ProviderBillingService * Update RemoveOrganizatinoFromProviderCommandTests * Update PreviewTaxAmountCommandTests * Update GetCloudOrganizationLicenseQueryTests * Update GetOrganizationWarningsQueryTests * Update StripePaymentServiceTests * Update ProviderBillingControllerTests * Update ProviderEventServiceTests * Update SubscriptionDeletedHandlerTests * Update SubscriptionUpdatedHandlerTests * Resolve Billing test failures I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit. * Resolve Core test failures * Run dotnet format * Remove unused provider migration * Fixed failing tests * Run dotnet format * Replace the old webhook secret key with new one (#6223) * Fix compilation failures in additions * Run dotnet format * Bump Stripe API version * Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand * Fix new code in main according to Stripe update * Fix InvoiceExtensions * Bump SDK version to match API Version * cleanup * fixing items missed after the merge * use expression body for all simple returns * forgot fixes, format, and pr feedback * claude pr feedback * pr feedback and cleanup * more claude feedback --------- Co-authored-by: Alex Morask <amorask@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
197 lines
6.0 KiB
C#
197 lines
6.0 KiB
C#
using Bit.Api.Billing.Models.Requests;
|
|
using Bit.Api.Billing.Models.Responses;
|
|
using Bit.Core.Billing.Organizations.Services;
|
|
using Bit.Core.Billing.Providers.Services;
|
|
using Bit.Core.Billing.Services;
|
|
using Bit.Core.Context;
|
|
using Bit.Core.Repositories;
|
|
using Bit.Core.Utilities;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace Bit.Api.Billing.Controllers;
|
|
|
|
[Route("organizations/{organizationId:guid}/billing")]
|
|
[Authorize("Application")]
|
|
public class OrganizationBillingController(
|
|
IBusinessUnitConverter businessUnitConverter,
|
|
ICurrentContext currentContext,
|
|
IOrganizationBillingService organizationBillingService,
|
|
IOrganizationRepository organizationRepository,
|
|
IStripePaymentService paymentService,
|
|
IPaymentHistoryService paymentHistoryService) : BaseBillingController
|
|
{
|
|
// TODO: Remove when pm-25379-use-new-organization-metadata-structure is removed.
|
|
[HttpGet("metadata")]
|
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
|
{
|
|
if (!await currentContext.OrganizationUser(organizationId))
|
|
{
|
|
return Error.Unauthorized();
|
|
}
|
|
|
|
var metadata = await organizationBillingService.GetMetadata(organizationId);
|
|
|
|
if (metadata == null)
|
|
{
|
|
return Error.NotFound();
|
|
}
|
|
|
|
return TypedResults.Ok(metadata);
|
|
}
|
|
|
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
|
[HttpGet("history")]
|
|
public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)
|
|
{
|
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
|
{
|
|
return Error.Unauthorized();
|
|
}
|
|
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return Error.NotFound();
|
|
}
|
|
|
|
var billingInfo = await paymentService.GetBillingHistoryAsync(organization);
|
|
|
|
return TypedResults.Ok(billingInfo);
|
|
}
|
|
|
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
|
[HttpGet("invoices")]
|
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
|
{
|
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return TypedResults.NotFound();
|
|
}
|
|
|
|
var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(
|
|
organization,
|
|
5,
|
|
status,
|
|
startAfter);
|
|
|
|
return TypedResults.Ok(invoices);
|
|
}
|
|
|
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
|
[HttpGet("transactions")]
|
|
public async Task<IResult> GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null)
|
|
{
|
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
|
{
|
|
return TypedResults.Unauthorized();
|
|
}
|
|
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return TypedResults.NotFound();
|
|
}
|
|
|
|
var transactions = await paymentHistoryService.GetTransactionHistoryAsync(
|
|
organization,
|
|
5,
|
|
startAfter);
|
|
|
|
return TypedResults.Ok(transactions);
|
|
}
|
|
|
|
// TODO: Can be removed once we do away with the organization-plans.component.
|
|
[HttpGet]
|
|
[SelfHosted(NotSelfHostedOnly = true)]
|
|
public async Task<IResult> GetBillingAsync(Guid organizationId)
|
|
{
|
|
if (!await currentContext.ViewBillingHistory(organizationId))
|
|
{
|
|
return Error.Unauthorized();
|
|
}
|
|
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return Error.NotFound();
|
|
}
|
|
|
|
var billingInfo = await paymentService.GetBillingAsync(organization);
|
|
|
|
var response = new BillingResponseModel(billingInfo);
|
|
|
|
return TypedResults.Ok(response);
|
|
}
|
|
|
|
// TODO: Migrate to Command / OrganizationBillingVNextController
|
|
[HttpPost("setup-business-unit")]
|
|
[SelfHosted(NotSelfHostedOnly = true)]
|
|
public async Task<IResult> SetupBusinessUnitAsync(
|
|
[FromRoute] Guid organizationId,
|
|
[FromBody] SetupBusinessUnitRequestBody requestBody)
|
|
{
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return Error.NotFound();
|
|
}
|
|
|
|
if (!await currentContext.OrganizationUser(organizationId))
|
|
{
|
|
return Error.Unauthorized();
|
|
}
|
|
|
|
var providerId = await businessUnitConverter.FinalizeConversion(
|
|
organization,
|
|
requestBody.UserId,
|
|
requestBody.Token,
|
|
requestBody.ProviderKey,
|
|
requestBody.OrganizationKey);
|
|
|
|
return TypedResults.Ok(providerId);
|
|
}
|
|
|
|
// TODO: Migrate to Command / OrganizationBillingVNextController
|
|
[HttpPost("change-frequency")]
|
|
[SelfHosted(NotSelfHostedOnly = true)]
|
|
public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(
|
|
[FromRoute] Guid organizationId,
|
|
[FromBody] ChangePlanFrequencyRequest request)
|
|
{
|
|
if (!await currentContext.EditSubscription(organizationId))
|
|
{
|
|
return Error.Unauthorized();
|
|
}
|
|
|
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
|
|
if (organization == null)
|
|
{
|
|
return Error.NotFound();
|
|
}
|
|
|
|
if (organization.PlanType == request.NewPlanType)
|
|
{
|
|
return Error.BadRequest("Organization is already on the requested plan frequency.");
|
|
}
|
|
|
|
await organizationBillingService.UpdateSubscriptionPlanFrequency(
|
|
organization,
|
|
request.NewPlanType);
|
|
|
|
return TypedResults.Ok();
|
|
}
|
|
}
|