1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 20:53:16 +00:00

[PM-25088] - refactor premium purchase endpoint (#6262)

* [PM-25088] add feature flag for new premium subscription flow

* [PM-25088] refactor premium endpoint

* forgot the punctuation change in the test

* [PM-25088] - pr feedback

* [PM-25088] - pr feedback round two
This commit is contained in:
Kyle Denney
2025-09-10 10:08:22 -05:00
committed by GitHub
parent d43b00dad9
commit a458db319e
25 changed files with 1309 additions and 21 deletions

View File

@@ -0,0 +1,13 @@
using Bit.Api.Utilities;
namespace Bit.Api.Billing.Attributes;
public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute
{
private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"];
public PaymentMethodTypeValidationAttribute() : base(_acceptedValues)
{
ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}";
}
}

View File

@@ -1,8 +1,11 @@
#nullable enable
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Premium;
using Bit.Core;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Entities;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@@ -16,6 +19,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
[SelfHosted(NotSelfHostedOnly = true)]
public class AccountBillingVNextController(
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
IGetCreditQuery getCreditQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
@@ -61,4 +65,17 @@ public class AccountBillingVNextController(
var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress);
return Handle(result);
}
[HttpPost("subscription")]
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
[InjectUser]
public async Task<IResult> CreateSubscriptionAsync(
[BindNever] User user,
[FromBody] PremiumCloudHostedSubscriptionRequest request)
{
var (paymentMethod, billingAddress, additionalStorageGb) = request.ToDomain();
var result = await createPremiumCloudHostedSubscriptionCommand.Run(
user, paymentMethod, billingAddress, additionalStorageGb);
return Handle(result);
}
}

View File

@@ -0,0 +1,38 @@
#nullable enable
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Premium;
using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Premium.Commands;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
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/self-host")]
[SelfHosted(SelfHostedOnly = true)]
public class SelfHostedAccountBillingController(
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
{
[HttpPost("license")]
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
[InjectUser]
public async Task<IResult> UploadLicenseAsync(
[BindNever] User user,
PremiumSelfHostedSubscriptionRequest request)
{
var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, request.License);
if (license == null)
{
throw new BadRequestException("Invalid license.");
}
var result = await createPremiumSelfHostedSubscriptionCommand.Run(user, license);
return Handle(result);
}
}

View File

@@ -0,0 +1,25 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.Billing.Attributes;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;
public class MinimalTokenizedPaymentMethodRequest
{
[Required]
[PaymentMethodTypeValidation]
public required string Type { get; set; }
[Required]
public required string Token { get; set; }
public TokenizedPaymentMethod ToDomain()
{
return new TokenizedPaymentMethod
{
Type = TokenizablePaymentMethodTypeExtensions.From(Type),
Token = Token
};
}
}

View File

@@ -1,6 +1,6 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities;
using Bit.Api.Billing.Attributes;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;
@@ -8,8 +8,7 @@ namespace Bit.Api.Billing.Models.Requests.Payment;
public class TokenizedPaymentMethodRequest
{
[Required]
[StringMatches("bankAccount", "card", "payPal",
ErrorMessage = "Payment method type must be one of: bankAccount, card, payPal")]
[PaymentMethodTypeValidation]
public required string Type { get; set; }
[Required]
@@ -21,14 +20,7 @@ public class TokenizedPaymentMethodRequest
{
var paymentMethod = new TokenizedPaymentMethod
{
Type = Type switch
{
"bankAccount" => TokenizablePaymentMethodType.BankAccount,
"card" => TokenizablePaymentMethodType.Card,
"payPal" => TokenizablePaymentMethodType.PayPal,
_ => throw new InvalidOperationException(
$"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}")
},
Type = TokenizablePaymentMethodTypeExtensions.From(Type),
Token = Token
};

View File

@@ -0,0 +1,26 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Premium;
public class PremiumCloudHostedSubscriptionRequest
{
[Required]
public required MinimalTokenizedPaymentMethodRequest TokenizedPaymentMethod { get; set; }
[Required]
public required MinimalBillingAddressRequest BillingAddress { get; set; }
[Range(0, 99)]
public short AdditionalStorageGb { get; set; } = 0;
public (TokenizedPaymentMethod, BillingAddress, short) ToDomain()
{
var paymentMethod = TokenizedPaymentMethod.ToDomain();
var billingAddress = BillingAddress.ToDomain();
return (paymentMethod, billingAddress, AdditionalStorageGb);
}
}

View File

@@ -0,0 +1,10 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Billing.Models.Requests.Premium;
public class PremiumSelfHostedSubscriptionRequest
{
[Required]
public required IFormFile License { get; set; }
}