Skip to content

Commit 9b7af51

Browse files
Merge branch 'main' into billing/aspire
2 parents f953e5e + cfd5bed commit 9b7af51

File tree

133 files changed

+24788
-2559
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

133 files changed

+24788
-2559
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ jobs:
126126
with:
127127
cache: "npm"
128128
cache-dependency-path: "**/package-lock.json"
129-
node-version: "16"
129+
node-version: "24"
130130

131131
- name: Print environment
132132
run: |

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,3 @@ jobs:
9999
name: "Version ${{ needs.setup.outputs.release_version }}"
100100
body: "<insert release notes here>"
101101
token: ${{ secrets.GITHUB_TOKEN }}
102-
draft: true

bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,16 @@
99
using Bit.Core.AdminConsole.Enums.Provider;
1010
using Bit.Core.AdminConsole.Repositories;
1111
using Bit.Core.Billing;
12-
using Bit.Core.Billing.Caches;
1312
using Bit.Core.Billing.Constants;
1413
using Bit.Core.Billing.Enums;
1514
using Bit.Core.Billing.Extensions;
16-
using Bit.Core.Billing.Models;
1715
using Bit.Core.Billing.Payment.Models;
1816
using Bit.Core.Billing.Pricing;
1917
using Bit.Core.Billing.Providers.Entities;
2018
using Bit.Core.Billing.Providers.Models;
2119
using Bit.Core.Billing.Providers.Repositories;
2220
using Bit.Core.Billing.Providers.Services;
2321
using Bit.Core.Billing.Services;
24-
using Bit.Core.Billing.Tax.Models;
2522
using Bit.Core.Enums;
2623
using Bit.Core.Exceptions;
2724
using Bit.Core.Repositories;
@@ -51,7 +48,6 @@ public class ProviderBillingService(
5148
IProviderOrganizationRepository providerOrganizationRepository,
5249
IProviderPlanRepository providerPlanRepository,
5350
IProviderUserRepository providerUserRepository,
54-
ISetupIntentCache setupIntentCache,
5551
IStripeAdapter stripeAdapter,
5652
ISubscriberService subscriberService)
5753
: IProviderBillingService
@@ -518,6 +514,7 @@ public async Task<Customer> SetupCustomer(
518514
}
519515

520516
var braintreeCustomerId = "";
517+
var setupIntentId = "";
521518

522519
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
523520
switch (paymentMethod.Type)
@@ -539,7 +536,7 @@ public async Task<Customer> SetupCustomer(
539536
throw new BillingException();
540537
}
541538

542-
await setupIntentCache.Set(provider.Id, setupIntent.Id);
539+
setupIntentId = setupIntent.Id;
543540
break;
544541
}
545542
case TokenizablePaymentMethodType.Card:
@@ -558,7 +555,15 @@ public async Task<Customer> SetupCustomer(
558555

559556
try
560557
{
561-
return await stripeAdapter.CreateCustomerAsync(options);
558+
var customer = await stripeAdapter.CreateCustomerAsync(options);
559+
560+
if (!string.IsNullOrEmpty(setupIntentId))
561+
{
562+
await stripeAdapter.UpdateSetupIntentAsync(setupIntentId,
563+
new SetupIntentUpdateOptions { Customer = customer.Id });
564+
}
565+
566+
return customer;
562567
}
563568
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
564569
{
@@ -577,12 +582,10 @@ async Task Revert()
577582
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
578583
switch (paymentMethod.Type)
579584
{
580-
case TokenizablePaymentMethodType.BankAccount:
585+
case TokenizablePaymentMethodType.BankAccount when !string.IsNullOrEmpty(setupIntentId):
581586
{
582-
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
583587
await stripeAdapter.CancelSetupIntentAsync(setupIntentId,
584588
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
585-
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
586589
break;
587590
}
588591
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
@@ -635,17 +638,18 @@ public async Task<Subscription> SetupSubscription(
635638
});
636639
}
637640

638-
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
641+
var setupIntents = await stripeAdapter.ListSetupIntentsAsync(new SetupIntentListOptions
642+
{
643+
Customer = customer.Id,
644+
Expand = ["data.payment_method"]
645+
});
639646

640-
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
641-
? await stripeAdapter.GetSetupIntentAsync(setupIntentId,
642-
new SetupIntentGetOptions { Expand = ["payment_method"] })
643-
: null;
647+
var hasUnverifiedBankAccount = setupIntents?.Any(si => si.IsUnverifiedBankAccount()) ?? false;
644648

645649
var usePaymentMethod =
646650
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
647651
customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||
648-
setupIntent?.IsUnverifiedBankAccount() == true;
652+
hasUnverifiedBankAccount;
649653

650654
int? trialPeriodDays = provider.Type switch
651655
{
@@ -699,19 +703,6 @@ public async Task<Subscription> SetupSubscription(
699703
}
700704
}
701705

702-
public async Task UpdatePaymentMethod(
703-
Provider provider,
704-
TokenizedPaymentSource tokenizedPaymentSource,
705-
TaxInformation taxInformation)
706-
{
707-
await Task.WhenAll(
708-
subscriberService.UpdatePaymentSource(provider, tokenizedPaymentSource),
709-
subscriberService.UpdateTaxInformation(provider, taxInformation));
710-
711-
await stripeAdapter.UpdateSubscriptionAsync(provider.GatewaySubscriptionId,
712-
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
713-
}
714-
715706
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
716707
{
717708
var (provider, updatedPlanConfigurations) = command;

bitwarden_license/src/Scim/Controllers/v2/UsersController.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
// FIXME: Update this file to be null safe and then delete the line below
22
#nullable disable
33

4+
using Bit.Core;
5+
using Bit.Core.AdminConsole.Models.Data;
46
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
57
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
6-
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1;
8+
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2;
79
using Bit.Core.Enums;
810
using Bit.Core.Exceptions;
911
using Bit.Core.Repositories;
12+
using Bit.Core.Services;
1013
using Bit.Scim.Models;
1114
using Bit.Scim.Users.Interfaces;
1215
using Bit.Scim.Utilities;
1316
using Microsoft.AspNetCore.Authorization;
1417
using Microsoft.AspNetCore.Mvc;
18+
using IRevokeOrganizationUserCommand = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v1.IRevokeOrganizationUserCommand;
19+
using IRevokeOrganizationUserCommandV2 = Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RevokeUser.v2.IRevokeOrganizationUserCommand;
1520

1621
namespace Bit.Scim.Controllers.v2;
1722

@@ -28,14 +33,18 @@ public class UsersController : Controller
2833
private readonly IPostUserCommand _postUserCommand;
2934
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
3035
private readonly IRevokeOrganizationUserCommand _revokeOrganizationUserCommand;
36+
private readonly IFeatureService _featureService;
37+
private readonly IRevokeOrganizationUserCommandV2 _revokeOrganizationUserCommandV2;
3138

3239
public UsersController(IOrganizationUserRepository organizationUserRepository,
3340
IGetUsersListQuery getUsersListQuery,
3441
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
3542
IPatchUserCommand patchUserCommand,
3643
IPostUserCommand postUserCommand,
3744
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
38-
IRevokeOrganizationUserCommand revokeOrganizationUserCommand)
45+
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
46+
IFeatureService featureService,
47+
IRevokeOrganizationUserCommandV2 revokeOrganizationUserCommandV2)
3948
{
4049
_organizationUserRepository = organizationUserRepository;
4150
_getUsersListQuery = getUsersListQuery;
@@ -44,6 +53,8 @@ public UsersController(IOrganizationUserRepository organizationUserRepository,
4453
_postUserCommand = postUserCommand;
4554
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
4655
_revokeOrganizationUserCommand = revokeOrganizationUserCommand;
56+
_featureService = featureService;
57+
_revokeOrganizationUserCommandV2 = revokeOrganizationUserCommandV2;
4758
}
4859

4960
[HttpGet("{id}")]
@@ -100,7 +111,33 @@ public async Task<IActionResult> Put(Guid organizationId, Guid id, [FromBody] Sc
100111
}
101112
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
102113
{
103-
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
114+
if (_featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2))
115+
{
116+
var results = await _revokeOrganizationUserCommandV2.RevokeUsersAsync(
117+
new RevokeOrganizationUsersRequest(
118+
organizationId,
119+
[id],
120+
new SystemUser(EventSystemUser.SCIM)));
121+
122+
var errors = results.Select(x => x.Result.Match(
123+
y => $"{y.Message} for user {x.Id}",
124+
_ => null))
125+
.Where(x => !string.IsNullOrWhiteSpace(x))
126+
.ToList();
127+
128+
if (errors.Count != 0)
129+
{
130+
return new BadRequestObjectResult(new ScimErrorResponseModel
131+
{
132+
Status = 400,
133+
Detail = string.Join(", ", errors)
134+
});
135+
}
136+
}
137+
else
138+
{
139+
await _revokeOrganizationUserCommand.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
140+
}
104141
}
105142

106143
// Have to get full details object for response model

bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Bit.Core.AdminConsole.Enums.Provider;
77
using Bit.Core.AdminConsole.Models.Data.Provider;
88
using Bit.Core.AdminConsole.Repositories;
9-
using Bit.Core.Billing.Caches;
109
using Bit.Core.Billing.Constants;
1110
using Bit.Core.Billing.Enums;
1211
using Bit.Core.Billing.Payment.Models;
@@ -934,17 +933,11 @@ public async Task SetupCustomer_WithBankAccount_Error_Reverts(
934933
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
935934
.Throws<StripeException>();
936935

937-
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id");
938-
939936
await Assert.ThrowsAsync<StripeException>(() =>
940937
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
941938

942-
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
943-
944939
await stripeAdapter.Received(1).CancelSetupIntentAsync("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
945940
options.CancellationReason == "abandoned"));
946-
947-
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
948941
}
949942

950943
[Theory, BitAutoData]
@@ -1031,7 +1024,8 @@ public async Task SetupCustomer_WithBankAccount_Success(
10311024

10321025
Assert.Equivalent(expected, actual);
10331026

1034-
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
1027+
await stripeAdapter.Received(1).UpdateSetupIntentAsync("setup_intent_id",
1028+
Arg.Is<SetupIntentUpdateOptions>(options => options.Customer == expected.Id));
10351029
}
10361030

10371031
[Theory, BitAutoData]
@@ -1532,15 +1526,12 @@ public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds(
15321526

15331527
var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };
15341528

1535-
1536-
const string setupIntentId = "seti_123";
1537-
1538-
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
1539-
1540-
sutProvider.GetDependency<IStripeAdapter>().GetSetupIntentAsync(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
1541-
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
1529+
sutProvider.GetDependency<IStripeAdapter>().ListSetupIntentsAsync(Arg.Is<SetupIntentListOptions>(options =>
1530+
options.Customer == customer.Id &&
1531+
options.Expand.Contains("data.payment_method"))).Returns([
1532+
new SetupIntent
15421533
{
1543-
Id = setupIntentId,
1534+
Id = "seti_123",
15441535
Status = "requires_action",
15451536
NextAction = new SetupIntentNextAction
15461537
{
@@ -1550,7 +1541,8 @@ public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds(
15501541
{
15511542
UsBankAccount = new PaymentMethodUsBankAccount()
15521543
}
1553-
});
1544+
}
1545+
]);
15541546

15551547
sutProvider.GetDependency<IStripeAdapter>().CreateSubscriptionAsync(Arg.Is<SubscriptionCreateOptions>(
15561548
sub =>

bitwarden_license/test/Scim.IntegrationTest/Controllers/v2/UsersControllerTests.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,9 +394,18 @@ public async Task Post_ExistingData_Conflict(string email, string externalId)
394394
Assert.Equal(_initialUserCount, databaseContext.OrganizationUsers.Count());
395395
}
396396

397-
[Fact]
398-
public async Task Put_RevokeUser_Success()
397+
[Theory]
398+
[InlineData(true)]
399+
[InlineData(false)]
400+
public async Task Put_RevokeUser_Success(bool scimRevokeV2Enabled)
399401
{
402+
var localFactory = new ScimApplicationFactory();
403+
localFactory.SubstituteService((IFeatureService featureService)
404+
=> featureService.IsEnabled(FeatureFlagKeys.ScimRevokeV2)
405+
.Returns(scimRevokeV2Enabled));
406+
407+
localFactory.ReinitializeDbForTests(localFactory.GetDatabaseContext());
408+
400409
var organizationUserId = ScimApplicationFactory.TestOrganizationUserId2;
401410
var inputModel = new ScimUserRequestModel
402411
{
@@ -418,13 +427,13 @@ public async Task Put_RevokeUser_Success()
418427
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
419428
};
420429

421-
var context = await _factory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);
430+
var context = await localFactory.UsersPutAsync(ScimApplicationFactory.TestOrganizationId1, organizationUserId, inputModel);
422431

423432
var responseModel = JsonSerializer.Deserialize<ScimUserResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
424433
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
425434
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
426435

427-
var databaseContext = _factory.GetDatabaseContext();
436+
var databaseContext = localFactory.GetDatabaseContext();
428437
var revokedUser = databaseContext.OrganizationUsers.FirstOrDefault(g => g.Id == organizationUserId);
429438
Assert.Equal(OrganizationUserStatusType.Revoked, revokedUser.Status);
430439
}

0 commit comments

Comments
 (0)