1
0
mirror of https://github.com/bitwarden/server synced 2025-12-27 05:33:17 +00:00

[PM-21827] Implement mechanism to suspend currently unpaid providers (#6119)

* Manually suspend provider and set cancel_at when we receive 'suspend_provider' metadata update

* Run dotnet format'
This commit is contained in:
Alex Morask
2025-07-24 11:50:09 -05:00
committed by GitHub
parent 05398ad8a4
commit c503ecbefc
2 changed files with 198 additions and 137 deletions

View File

@@ -1,4 +1,5 @@
using Bit.Billing.Constants;
using System.Globalization;
using Bit.Billing.Constants;
using Bit.Billing.Jobs;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
@@ -316,7 +317,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
private async Task HandleUnpaidProviderSubscriptionAsync(
Guid providerId,
Event parsedEvent,
Subscription subscription)
Subscription currentSubscription)
{
var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover);
@@ -338,26 +339,43 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
if (parsedEvent.Data.PreviousAttributes != null)
{
if (parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() as Subscription is
{
Status:
StripeSubscriptionStatus.Trialing or
StripeSubscriptionStatus.Active or
StripeSubscriptionStatus.PastDue
} && subscription is
{
Status: StripeSubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
})
var previousSubscription = parsedEvent.Data.PreviousAttributes.ToObject<Subscription>() as Subscription;
var updateIsSubscriptionGoingUnpaid = previousSubscription is
{
if (subscription.TestClock != null)
Status:
StripeSubscriptionStatus.Trialing or
StripeSubscriptionStatus.Active or
StripeSubscriptionStatus.PastDue
} && currentSubscription is
{
Status: StripeSubscriptionStatus.Unpaid,
LatestInvoice.BillingReason: "subscription_cycle" or "subscription_create"
};
var updateIsManualSuspensionViaMetadata = CheckForManualSuspensionViaMetadata(
previousSubscription, currentSubscription);
if (updateIsSubscriptionGoingUnpaid || updateIsManualSuspensionViaMetadata)
{
if (currentSubscription.TestClock != null)
{
await WaitForTestClockToAdvanceAsync(subscription.TestClock);
await WaitForTestClockToAdvanceAsync(currentSubscription.TestClock);
}
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
await _stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) });
var now = currentSubscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAt = now.AddDays(7) };
if (updateIsManualSuspensionViaMetadata)
{
subscriptionUpdateOptions.Metadata = new Dictionary<string, string>
{
["suspended_provider_via_webhook_at"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)
};
}
await _stripeFacade.UpdateSubscription(currentSubscription.Id, subscriptionUpdateOptions);
}
}
}
@@ -379,4 +397,37 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
}
}
}
private static bool CheckForManualSuspensionViaMetadata(
Subscription? previousSubscription,
Subscription currentSubscription)
{
/*
* When metadata on a subscription is updated, we'll receive an event that has:
* Previous Metadata: { newlyAddedKey: null }
* Current Metadata: { newlyAddedKey: newlyAddedValue }
*
* As such, our check for a manual suspension must ensure that the 'previous_attributes' does contain the
* 'metadata' property, but also that the "suspend_provider" key in that metadata is set to null.
*
* If we don't do this and instead do a null coalescing check on 'previous_attributes?.metadata?.TryGetValue',
* we'll end up marking an event where 'previous_attributes.metadata' = null (which could be any subscription update
* that does not update the metadata) the same as a manual suspension.
*/
const string key = "suspend_provider";
if (previousSubscription is not { Metadata: not null } ||
!previousSubscription.Metadata.TryGetValue(key, out var previousValue))
{
return false;
}
if (previousValue == null)
{
return !string.IsNullOrEmpty(
currentSubscription.Metadata.TryGetValue(key, out var currentValue) ? currentValue : null);
}
return false;
}
}