Skip to content

[PM-32477]PremiumStatusChanged Push Notification#7198

Open
cyprain-okeke wants to merge 8 commits intomainfrom
billing/pm-32477/premiumStatusChanged-push-notification
Open

[PM-32477]PremiumStatusChanged Push Notification#7198
cyprain-okeke wants to merge 8 commits intomainfrom
billing/pm-32477/premiumStatusChanged-push-notification

Conversation

@cyprain-okeke
Copy link
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-32477

📔 Objective

Summary
Implement premium-status push notifications across billing flows so clients stay in sync whenever a user’s premium state changes.

Details

New adapter API for premium changes

  • Extend IPushNotificationAdapter with NotifyPremiumStatusChangedAsync(User user).
  • Implement in PushNotificationAdapter by delegating to IPushNotificationService.PushPremiumStatusChangedAsync(user).

Stripe payment and subscription handlers

  • PaymentSucceededHandler
    • Inject IUserRepository.
    • After EnablePremiumAsync(userId, subscription.GetCurrentPeriodEnd()), load the user and call NotifyPremiumStatusChangedAsync(user) so successful payments trigger a premium-status push.

SubscriptionDeletedHandler

  • Inject IUserRepository and IPushNotificationAdapter.
  • When a user subscription is deleted (userId present), call DisablePremiumAsync and then fetch the user and send NotifyPremiumStatusChangedAsync(user).

SubscriptionUpdatedHandler

  • Inject IUserRepository and IPushNotificationAdapter.
  • In DisableSubscriberAsync user branch: disable premium, fetch the user, then call NotifyPremiumStatusChangedAsync(user).
  • In EnableSubscriberAsync user branch: enable premium, fetch the user, then call NotifyPremiumStatusChangedAsync(user).

Premium subscription commands

  • CreatePremiumCloudHostedSubscriptionCommand

    • After saving the user and PushSyncVaultAsync(user.Id), call PushPremiumStatusChangedAsync(user) so new/updated cloud subscriptions immediately push the status change.
  • CreatePremiumSelfHostedSubscriptionCommand

  • After writing the license and saving the user, call PushPremiumStatusChangedAsync(user) to broadcast the new premium status for self-hosted subscriptions.

  • UpgradePremiumToOrganizationCommand

    • Inject IPushNotificationService.
    • After downgrading the user’s personal premium while upgrading to an organization, call PushPremiumStatusChangedAsync(user) so clients see the user’s premium flag change.

Tests

  • Update billing service tests to:

    • Use IUserRepository to return a User instance when subscriptions are enabled/disabled.
    • Assert IPushNotificationAdapter.NotifyPremiumStatusChangedAsync(user) is called in subscription deleted/updated scenarios.
  • Update premium command tests to:

    • Assert IPushNotificationService.PushPremiumStatusChangedAsync is called with a User whose Premium flag matches the expected post-command state (true for new subscriptions, false when upgrading to an organization).

📸 Screenshots

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

Logo
Checkmarx One – Scan Summary & Details20a61883-b64f-475e-ba44-258ac0cf1900


New Issues (2) Checkmarx found the following issues in this Pull Request
# Severity Issue Source File / Package Checkmarx Insight
1 MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1569
detailsMethod at line 1569 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector
2 MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1403
detailsMethod at line 1403 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
Attack Vector

Fixed Issues (2) Great job! The following issues were fixed in this Pull Request
Severity Issue Source File / Package
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 105
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 293

@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 61.36364% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.43%. Comparing base (20d94c3) to head (cd1f7e8).

Files with missing lines Patch % Lines
...ervices/Implementations/PushNotificationAdapter.cs 0.00% 12 Missing ⚠️
...lling/Services/Implementations/LicensingService.cs 20.00% 12 Missing ⚠️
...ervices/Implementations/PaymentSucceededHandler.cs 0.00% 7 Missing ⚠️
src/Notifications/HubHelpers.cs 66.66% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7198   +/-   ##
=======================================
  Coverage   57.43%   57.43%           
=======================================
  Files        2032     2032           
  Lines       89377    89460   +83     
  Branches     7944     7948    +4     
=======================================
+ Hits        51331    51382   +51     
- Misses      36203    36234   +31     
- Partials     1843     1844    +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.


await userService.EnablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
var user = await userRepository.GetByIdAsync(userId.Value);
await pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);
Copy link
Contributor

@amorask-bitwarden amorask-bitwarden Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ 2 things we should clean up here:

  1. userService.EnablePremiumAsync makes no guarantees the User exists. It just skips the operation if the user is null, which means line 120 could result in an NRE. Not only that, but if the User that comes back from the DB is null, the Enable would be followed by us sending the notification anyway, which is incorrect.
  2. This will never be hit for users on the new price because of the missed Price adaptation on line 113 - we should fix that.

ExcludeCurrentContext = false,
});

Task PushPremiumStatusChangedAsync(Entities.User user)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ We're no longer adding methods like this to the shared IPushNotificationService. Please follow the established pattern in the the Billing application's PushNotificationAdapter so that we own the code.

ExcludeCurrentContext = false,
});

public Task NotifyPremiumStatusChangedAsync(User user) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Related to this comment, this should follow the pattern of the methods above.

{
await _userService.DisablePremiumAsync(userId.Value, subscription.GetCurrentPeriodEnd());
var user = await _userRepository.GetByIdAsync(userId.Value);
await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Same issue as here.

{
await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);
var user = await _userRepository.GetByIdAsync(userId.Value);
await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Same issue as here.

{
await _userService.EnablePremiumAsync(userId.Value, currentPeriodEnd);
var user = await _userRepository.GetByIdAsync(userId.Value);
await _pushNotificationAdapter.NotifyPremiumStatusChangedAsync(user!);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Same issue as here.


await userService.SaveUserAsync(user);
await pushNotificationService.PushSyncVaultAsync(user.Id);
await pushNotificationService.PushPremiumStatusChangedAsync(user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Wouldn't this be redundant? I assume a full sync would re-pull Premium status, right?


await userService.SaveUserAsync(user);
await pushNotificationService.PushSyncVaultAsync(user.Id);
await pushNotificationService.PushPremiumStatusChangedAsync(user);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Same question as here.

{
public Guid UserId { get; set; }
public bool Premium { get; set; }
public DateTime? PremiumExpirationDate { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Is there a client-side use-case for including the expiration date?

public Guid TargetOrganizationUserId { get; set; }
}

public class PremiumStatusPushNotification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Please review the comments on lines 7 and 8 of this file regarding the location this file should live in.

Copy link
Contributor

@djsmith85 djsmith85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like after the comments from @amorask-bitwarden are addressed, a review from platform might not be necessary anymore. In case they do, please re-request a review.

@cyprain-okeke cyprain-okeke requested review from amorask-bitwarden and djsmith85 and removed request for djsmith85 March 12, 2026 12:22
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants