1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-23687] Support free organizations on Payment Details page (#6084)

* Resolve JSON serialization bug in OneOf converters and organize pricing models

* Support free organizations for payment method and billing address flows

* Run dotnet format
This commit is contained in:
Alex Morask
2025-07-14 12:39:49 -05:00
committed by GitHub
parent 0e4e060f22
commit d914ab8a98
30 changed files with 575 additions and 316 deletions

View File

@@ -3,6 +3,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Extensions;
using Microsoft.Extensions.Logging;
@@ -16,14 +17,15 @@ using static StripeConstants;
public class UpdateBillingAddressCommandTests
{
private readonly IStripeAdapter _stripeAdapter;
private readonly ISubscriberService _subscriberService = Substitute.For<ISubscriberService>();
private readonly IStripeAdapter _stripeAdapter = Substitute.For<IStripeAdapter>();
private readonly UpdateBillingAddressCommand _command;
public UpdateBillingAddressCommandTests()
{
_stripeAdapter = Substitute.For<IStripeAdapter>();
_command = new UpdateBillingAddressCommand(
Substitute.For<ILogger<UpdateBillingAddressCommand>>(),
_subscriberService,
_stripeAdapter);
}
@@ -86,6 +88,66 @@ public class UpdateBillingAddressCommandTests
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
[Fact]
public async Task Run_PersonalOrganization_NoCurrentCustomer_MakesCorrectInvocations_ReturnsBillingAddress()
{
var organization = new Organization
{
PlanType = PlanType.FamiliesAnnually,
GatewaySubscriptionId = "sub_123"
};
var input = new BillingAddress
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Suite 100",
City = "New York",
State = "NY"
};
var customer = new Customer
{
Address = new Address
{
Country = "US",
PostalCode = "12345",
Line1 = "123 Main St.",
Line2 = "Suite 100",
City = "New York",
State = "NY"
},
Subscriptions = new StripeList<Subscription>
{
Data =
[
new Subscription
{
Id = organization.GatewaySubscriptionId,
AutomaticTax = new SubscriptionAutomaticTax { Enabled = false }
}
]
}
};
_stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(options =>
options.Address.Matches(input) &&
options.HasExpansions("subscriptions")
)).Returns(customer);
var result = await _command.Run(organization, input);
Assert.True(result.IsT0);
var output = result.AsT0;
Assert.Equivalent(input, output);
await _subscriberService.Received(1).CreateStripeCustomer(organization);
await _stripeAdapter.Received(1).SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
Arg.Is<SubscriptionUpdateOptions>(options => options.AutomaticTax.Enabled == true));
}
[Fact]
public async Task Run_BusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress()
{

View File

@@ -45,7 +45,8 @@ public class UpdatePaymentMethodCommandTests
{
var organization = new Organization
{
Id = Guid.NewGuid()
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer
@@ -100,13 +101,75 @@ public class UpdatePaymentMethodCommandTests
}
[Fact]
public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount()
public async Task Run_BankAccount_NoCurrentCustomer_MakesCorrectInvocations_ReturnsMaskedBankAccount()
{
var organization = new Organization
{
Id = Guid.NewGuid()
};
var customer = new Customer
{
Address = new Address
{
Country = "US",
PostalCode = "12345"
},
Metadata = new Dictionary<string, string>()
};
_subscriberService.GetCustomer(organization).Returns(customer);
const string token = "TOKEN";
var setupIntent = new SetupIntent
{
Id = "seti_123",
PaymentMethod =
new PaymentMethod
{
Type = "us_bank_account",
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
},
NextAction = new SetupIntentNextAction
{
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
},
Status = "requires_action"
};
_stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == token && options.HasExpansions("data.payment_method"))).Returns([setupIntent]);
var result = await _command.Run(organization,
new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = token }, new BillingAddress
{
Country = "US",
PostalCode = "12345"
});
Assert.True(result.IsT0);
var maskedPaymentMethod = result.AsT0;
Assert.True(maskedPaymentMethod.IsT0);
var maskedBankAccount = maskedPaymentMethod.AsT0;
Assert.Equal("Chase", maskedBankAccount.BankName);
Assert.Equal("9999", maskedBankAccount.Last4);
Assert.False(maskedBankAccount.Verified);
await _subscriberService.Received(1).CreateStripeCustomer(organization);
await _setupIntentCache.Received(1).Set(organization.Id, setupIntent.Id);
}
[Fact]
public async Task Run_BankAccount_StripeToPayPal_MakesCorrectInvocations_ReturnsMaskedBankAccount()
{
var organization = new Organization
{
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer
{
Address = new Address
@@ -170,7 +233,8 @@ public class UpdatePaymentMethodCommandTests
{
var organization = new Organization
{
Id = Guid.NewGuid()
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer
@@ -227,7 +291,8 @@ public class UpdatePaymentMethodCommandTests
{
var organization = new Organization
{
Id = Guid.NewGuid()
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer
@@ -282,7 +347,8 @@ public class UpdatePaymentMethodCommandTests
{
var organization = new Organization
{
Id = Guid.NewGuid()
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer
@@ -343,7 +409,8 @@ public class UpdatePaymentMethodCommandTests
{
var organization = new Organization
{
Id = Guid.NewGuid()
Id = Guid.NewGuid(),
GatewayCustomerId = "cus_123"
};
var customer = new Customer

View File

@@ -25,6 +25,27 @@ public class MaskedPaymentMethodTests
Assert.Equivalent(input.AsT0, output.AsT0);
}
[Fact]
public void Write_Read_BankAccount_WithOptions_Succeeds()
{
MaskedPaymentMethod input = new MaskedBankAccount
{
BankName = "Chase",
Last4 = "9999",
Verified = true
};
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
var json = JsonSerializer.Serialize(input, jsonSerializerOptions);
var output = JsonSerializer.Deserialize<MaskedPaymentMethod>(json, jsonSerializerOptions);
Assert.NotNull(output);
Assert.True(output.IsT0);
Assert.Equivalent(input.AsT0, output.AsT0);
}
[Fact]
public void Write_Read_Card_Succeeds()
{

View File

@@ -8,6 +8,7 @@ using Bit.Core.Test.Billing.Extensions;
using Braintree;
using Microsoft.Extensions.Logging;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Stripe;
using Xunit;
using Customer = Stripe.Customer;
@@ -35,6 +36,23 @@ public class GetPaymentMethodQueryTests
_subscriberService);
}
[Fact]
public async Task Run_NoCustomer_ReturnsNull()
{
var organization = new Organization
{
Id = Guid.NewGuid()
};
_subscriberService.GetCustomer(organization,
Arg.Is<CustomerGetOptions>(options =>
options.HasExpansions("default_source", "invoice_settings.default_payment_method"))).ReturnsNull();
var maskedPaymentMethod = await _query.Run(organization);
Assert.Null(maskedPaymentMethod);
}
[Fact]
public async Task Run_NoPaymentMethod_ReturnsNull()
{