mirror of
https://github.com/bitwarden/server
synced 2025-12-21 02:33:30 +00:00
[PM-21881] Manage payment details outside of checkout (#6032)
* Add feature flag * Further establish billing command pattern and use in PreviewTaxAmountCommand * Add billing address models/commands/queries/tests * Update TypeReadingJsonConverter to account for new union types * Add payment method models/commands/queries/tests * Add credit models/commands/queries/tests * Add command/query registrations * Add new endpoints to support new command model and payment functionality * Run dotnet format * Add InjectUserAttribute for easier AccountBillilngVNextController handling * Add InjectOrganizationAttribute for easier OrganizationBillingVNextController handling * Add InjectProviderAttribute for easier ProviderBillingVNextController handling * Add XML documentation for billing command pipeline * Fix StripeConstants post-nullability * More nullability cleanup * Run dotnet format
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using Bit.Core.Models.Api;
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Models.Api;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -6,20 +8,50 @@ namespace Bit.Api.Billing.Controllers;
|
||||
|
||||
public abstract class BaseBillingController : Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes the result of a billing command and converts it to an appropriate HTTP result response.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Result to response mappings:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><typeparamref name="T"/>: 200 OK</description></item>
|
||||
/// <item><description><see cref="Core.Billing.Commands.BadRequest"/>: 400 BAD_REQUEST</description></item>
|
||||
/// <item><description><see cref="Core.Billing.Commands.Conflict"/>: 409 CONFLICT</description></item>
|
||||
/// <item><description><see cref="Unhandled"/>: 500 INTERNAL_SERVER_ERROR</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">The type of the successful result.</typeparam>
|
||||
/// <param name="result">The result of executing the billing command.</param>
|
||||
/// <returns>An HTTP result response representing the outcome of the command execution.</returns>
|
||||
protected static IResult Handle<T>(BillingCommandResult<T> result) =>
|
||||
result.Match<IResult>(
|
||||
TypedResults.Ok,
|
||||
badRequest => Error.BadRequest(badRequest.Response),
|
||||
conflict => Error.Conflict(conflict.Response),
|
||||
unhandled => Error.ServerError(unhandled.Response, unhandled.Exception));
|
||||
|
||||
protected static class Error
|
||||
{
|
||||
public static BadRequest<ErrorResponseModel> BadRequest(Dictionary<string, IEnumerable<string>> errors) =>
|
||||
TypedResults.BadRequest(new ErrorResponseModel(errors));
|
||||
|
||||
public static BadRequest<ErrorResponseModel> BadRequest(string message) =>
|
||||
TypedResults.BadRequest(new ErrorResponseModel(message));
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> Conflict(string message) =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel(message),
|
||||
statusCode: StatusCodes.Status409Conflict);
|
||||
|
||||
public static NotFound<ErrorResponseModel> NotFound() =>
|
||||
TypedResults.NotFound(new ErrorResponseModel("Resource not found."));
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> ServerError(string message = "Something went wrong with your request. Please contact support.") =>
|
||||
public static JsonHttpResult<ErrorResponseModel> ServerError(
|
||||
string message = "Something went wrong with your request. Please contact support for assistance.",
|
||||
Exception? exception = null) =>
|
||||
TypedResults.Json(
|
||||
new ErrorResponseModel(message),
|
||||
exception == null ? new ErrorResponseModel(message) : new ErrorResponseModel(message)
|
||||
{
|
||||
ExceptionMessage = exception.Message,
|
||||
ExceptionStackTrace = exception.StackTrace
|
||||
},
|
||||
statusCode: StatusCodes.Status500InternalServerError);
|
||||
|
||||
public static JsonHttpResult<ErrorResponseModel> Unauthorized(string message = "Unauthorized.") =>
|
||||
|
||||
@@ -28,9 +28,6 @@ public class TaxController(
|
||||
|
||||
var result = await previewTaxAmountCommand.Run(parameters);
|
||||
|
||||
return result.Match<IResult>(
|
||||
taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }),
|
||||
badRequest => Error.BadRequest(badRequest.TranslationKey),
|
||||
unhandled => Error.ServerError(unhandled.TranslationKey));
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("account/billing/vnext")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class AccountBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||
{
|
||||
[HttpGet("credit")]
|
||||
[InjectUser]
|
||||
public async Task<IResult> GetCreditAsync(
|
||||
[BindNever] User user)
|
||||
{
|
||||
var credit = await getCreditQuery.Run(user);
|
||||
return TypedResults.Ok(credit);
|
||||
}
|
||||
|
||||
[HttpPost("credit/bitpay")]
|
||||
[InjectUser]
|
||||
public async Task<IResult> AddCreditViaBitPayAsync(
|
||||
[BindNever] User user,
|
||||
[FromBody] BitPayCreditRequest request)
|
||||
{
|
||||
var result = await createBitPayInvoiceForCreditCommand.Run(
|
||||
user,
|
||||
request.Amount,
|
||||
request.RedirectUrl);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
[InjectUser]
|
||||
public async Task<IResult> GetPaymentMethodAsync(
|
||||
[BindNever] User user)
|
||||
{
|
||||
var paymentMethod = await getPaymentMethodQuery.Run(user);
|
||||
return TypedResults.Ok(paymentMethod);
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
[InjectUser]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[BindNever] User user,
|
||||
[FromBody] TokenizedPaymentMethodRequest request)
|
||||
{
|
||||
var (paymentMethod, billingAddress) = request.ToDomain();
|
||||
var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Api.Billing.Models.Requirements;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
// ReSharper disable RouteTemplates.MethodMissingRouteParameters
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("organizations/{organizationId:guid}/billing/vnext")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class OrganizationBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
IGetBillingAddressQuery getBillingAddressQuery,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
||||
{
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpGet("address")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetBillingAddressAsync(
|
||||
[BindNever] Organization organization)
|
||||
{
|
||||
var billingAddress = await getBillingAddressQuery.Run(organization);
|
||||
return TypedResults.Ok(billingAddress);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpPut("address")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> UpdateBillingAddressAsync(
|
||||
[BindNever] Organization organization,
|
||||
[FromBody] BillingAddressRequest request)
|
||||
{
|
||||
var billingAddress = request.ToDomain();
|
||||
var result = await updateBillingAddressCommand.Run(organization, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpGet("credit")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetCreditAsync(
|
||||
[BindNever] Organization organization)
|
||||
{
|
||||
var credit = await getCreditQuery.Run(organization);
|
||||
return TypedResults.Ok(credit);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpPost("credit/bitpay")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> AddCreditViaBitPayAsync(
|
||||
[BindNever] Organization organization,
|
||||
[FromBody] BitPayCreditRequest request)
|
||||
{
|
||||
var result = await createBitPayInvoiceForCreditCommand.Run(
|
||||
organization,
|
||||
request.Amount,
|
||||
request.RedirectUrl);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpGet("payment-method")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> GetPaymentMethodAsync(
|
||||
[BindNever] Organization organization)
|
||||
{
|
||||
var paymentMethod = await getPaymentMethodQuery.Run(organization);
|
||||
return TypedResults.Ok(paymentMethod);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpPut("payment-method")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[BindNever] Organization organization,
|
||||
[FromBody] TokenizedPaymentMethodRequest request)
|
||||
{
|
||||
var (paymentMethod, billingAddress) = request.ToDomain();
|
||||
var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[BindNever] Organization organization,
|
||||
[FromBody] VerifyBankAccountRequest request)
|
||||
{
|
||||
var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
// ReSharper disable RouteTemplates.MethodMissingRouteParameters
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Route("providers/{providerId:guid}/billing/vnext")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProviderBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
IGetBillingAddressQuery getBillingAddressQuery,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
||||
{
|
||||
[HttpGet("address")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> GetBillingAddressAsync(
|
||||
[BindNever] Provider provider)
|
||||
{
|
||||
var billingAddress = await getBillingAddressQuery.Run(provider);
|
||||
return TypedResults.Ok(billingAddress);
|
||||
}
|
||||
|
||||
[HttpPut("address")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> UpdateBillingAddressAsync(
|
||||
[BindNever] Provider provider,
|
||||
[FromBody] BillingAddressRequest request)
|
||||
{
|
||||
var billingAddress = request.ToDomain();
|
||||
var result = await updateBillingAddressCommand.Run(provider, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("credit")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> GetCreditAsync(
|
||||
[BindNever] Provider provider)
|
||||
{
|
||||
var credit = await getCreditQuery.Run(provider);
|
||||
return TypedResults.Ok(credit);
|
||||
}
|
||||
|
||||
[HttpPost("credit/bitpay")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> AddCreditViaBitPayAsync(
|
||||
[BindNever] Provider provider,
|
||||
[FromBody] BitPayCreditRequest request)
|
||||
{
|
||||
var result = await createBitPayInvoiceForCreditCommand.Run(
|
||||
provider,
|
||||
request.Amount,
|
||||
request.RedirectUrl);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("payment-method")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> GetPaymentMethodAsync(
|
||||
[BindNever] Provider provider)
|
||||
{
|
||||
var paymentMethod = await getPaymentMethodQuery.Run(provider);
|
||||
return TypedResults.Ok(paymentMethod);
|
||||
}
|
||||
|
||||
[HttpPut("payment-method")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
||||
[BindNever] Provider provider,
|
||||
[FromBody] TokenizedPaymentMethodRequest request)
|
||||
{
|
||||
var (paymentMethod, billingAddress) = request.ToDomain();
|
||||
var result = await updatePaymentMethodCommand.Run(provider, paymentMethod, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[BindNever] Provider provider,
|
||||
[FromBody] VerifyBankAccountRequest request)
|
||||
{
|
||||
var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user