1
0
mirror of https://github.com/bitwarden/server synced 2025-12-22 11:13:27 +00:00

[PM-23309] Admin Console Credit is not Displaying Decimals (#6280)

* fix: update calculation to be decimal

* fix: update record type property to decimal

* tests: add tests to service and update test names
This commit is contained in:
Stephon Brown
2025-09-05 11:15:01 -04:00
committed by GitHub
parent 6d4129c6b7
commit 87bc9299e6
4 changed files with 183 additions and 48 deletions

View File

@@ -4,7 +4,7 @@ using Bit.Core.Billing.Tax.Models;
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
public record PaymentMethodResponse( public record PaymentMethodResponse(
long AccountCredit, decimal AccountCredit,
PaymentSource PaymentSource, PaymentSource PaymentSource,
string SubscriptionStatus, string SubscriptionStatus,
TaxInformation TaxInformation) TaxInformation TaxInformation)

View File

@@ -6,7 +6,7 @@ using Bit.Core.Billing.Tax.Models;
namespace Bit.Core.Billing.Models; namespace Bit.Core.Billing.Models;
public record PaymentMethod( public record PaymentMethod(
long AccountCredit, decimal AccountCredit,
PaymentSource PaymentSource, PaymentSource PaymentSource,
string SubscriptionStatus, string SubscriptionStatus,
TaxInformation TaxInformation) TaxInformation TaxInformation)

View File

@@ -345,7 +345,7 @@ public class SubscriberService(
return PaymentMethod.Empty; return PaymentMethod.Empty;
} }
var accountCredit = customer.Balance * -1 / 100; var accountCredit = customer.Balance * -1 / 100M;
var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer); var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer);

View File

@@ -329,13 +329,165 @@ public class SubscriberServiceTests
#endregion #endregion
#region GetPaymentMethod #region GetPaymentMethod
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException( public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) => SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null)); await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Braintree_NoDefaultPaymentMethod_ReturnsNull( public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization,
SutProvider<SubscriberService> sutProvider)
{
// Arrange
// Stripe reports balance in cents as a negative number for credit
const int stripeAccountBalance = -593; // $5.93 credit (negative cents)
const decimal creditAmount = 5.93M; // Same value in dollars
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(creditAmount, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
Organization organization, SutProvider<SubscriberService> sutProvider)
{
// Arrange
const int stripeAccountBalance = 0;
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(0, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
[Theory, BitAutoData]
public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
Organization organization, SutProvider<SubscriberService> sutProvider)
{
// Arrange
const int stripeAccountBalance = 593; // $5.93 charge balance
const decimal accountBalance = -5.93M; // account balance
var customer = new Customer
{
Balance = stripeAccountBalance,
Subscriptions = new StripeList<Subscription>()
{
Data =
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
},
InvoiceSettings = new CustomerInvoiceSettings
{
DefaultPaymentMethod = new PaymentMethod
{
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
}
}
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method")
&& options.Expand.Contains("subscriptions")
&& options.Expand.Contains("tax_ids")))
.Returns(customer);
// Act
var result = await sutProvider.Sut.GetPaymentMethod(organization);
// Assert
Assert.NotNull(result);
Assert.Equal(accountBalance, result.AccountCredit);
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
organization.GatewayCustomerId,
Arg.Is<CustomerGetOptions>(options =>
options.Expand.Contains("default_source") &&
options.Expand.Contains("invoice_settings.default_payment_method") &&
options.Expand.Contains("subscriptions") &&
options.Expand.Contains("tax_ids")));
}
#endregion
#region GetPaymentSource
[Theory, BitAutoData]
public async Task GetPaymentSource_NullSubscriber_ThrowsArgumentNullException(
SutProvider<SubscriberService> sutProvider) =>
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
[Theory, BitAutoData]
public async Task GetPaymentSource_Braintree_NoDefaultPaymentMethod_ReturnsNull(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
@@ -372,7 +524,7 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Braintree_PayPalAccount_Succeeds( public async Task GetPaymentSource_Braintree_PayPalAccount_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
@@ -421,7 +573,7 @@ public class SubscriberServiceTests
// TODO: Determine if we need to test Braintree.UsBankAccount // TODO: Determine if we need to test Braintree.UsBankAccount
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_BankAccountPaymentMethod_Succeeds( public async Task GetPaymentSource_Stripe_BankAccountPaymentMethod_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
@@ -455,7 +607,7 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_CardPaymentMethod_Succeeds( public async Task GetPaymentSource_Stripe_CardPaymentMethod_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
@@ -491,43 +643,37 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_SetupIntentForBankAccount_Succeeds( public async Task GetPaymentSource_Stripe_SetupIntentForBankAccount_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
var customer = new Customer var customer = new Customer { Id = provider.GatewayCustomerId };
{
Id = provider.GatewayCustomerId
};
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId, sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>( Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options => options.Expand.Contains("default_source") && options.Expand.Contains(
options.Expand.Contains("invoice_settings.default_payment_method"))) "invoice_settings.default_payment_method")))
.Returns(customer); .Returns(customer);
var setupIntent = new SetupIntent var setupIntent = new SetupIntent
{ {
Id = "setup_intent_id", Id = "setup_intent_id",
Status = "requires_action", Status = "requires_action",
NextAction = new SetupIntentNextAction NextAction =
new SetupIntentNextAction
{ {
VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits()
}, },
PaymentMethod = new PaymentMethod PaymentMethod = new PaymentMethod
{ {
UsBankAccount = new PaymentMethodUsBankAccount UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
{
BankName = "Chase",
Last4 = "9999"
}
} }
}; };
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id); sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntent.Id);
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id, Arg.Is<SetupIntentGetOptions>( sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntent.Id,
options => options.Expand.Contains("payment_method"))).Returns(setupIntent); Arg.Is<SetupIntentGetOptions>(options => options.Expand.Contains("payment_method"))).Returns(setupIntent);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -537,24 +683,19 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacyBankAccount_Succeeds( public async Task GetPaymentSource_Stripe_LegacyBankAccount_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
var customer = new Customer var customer = new Customer
{ {
DefaultSource = new BankAccount DefaultSource = new BankAccount { Status = "verified", BankName = "Chase", Last4 = "9999" }
{
Status = "verified",
BankName = "Chase",
Last4 = "9999"
}
}; };
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId, sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>( Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options => options.Expand.Contains("default_source") && options.Expand.Contains(
options.Expand.Contains("invoice_settings.default_payment_method"))) "invoice_settings.default_payment_method")))
.Returns(customer); .Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -565,25 +706,19 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacyCard_Succeeds( public async Task GetPaymentSource_Stripe_LegacyCard_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {
var customer = new Customer var customer = new Customer
{ {
DefaultSource = new Card DefaultSource = new Card { Brand = "Visa", Last4 = "9999", ExpMonth = 9, ExpYear = 2028 }
{
Brand = "Visa",
Last4 = "9999",
ExpMonth = 9,
ExpYear = 2028
}
}; };
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId, sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(provider.GatewayCustomerId,
Arg.Is<CustomerGetOptions>( Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
options => options.Expand.Contains("default_source") && options.Expand.Contains(
options.Expand.Contains("invoice_settings.default_payment_method"))) "invoice_settings.default_payment_method")))
.Returns(customer); .Returns(customer);
var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider); var paymentMethod = await sutProvider.Sut.GetPaymentSource(provider);
@@ -594,7 +729,7 @@ public class SubscriberServiceTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task GetPaymentMethod_Stripe_LegacySourceCard_Succeeds( public async Task GetPaymentSource_Stripe_LegacySourceCard_Succeeds(
Provider provider, Provider provider,
SutProvider<SubscriberService> sutProvider) SutProvider<SubscriberService> sutProvider)
{ {