-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PM-16684] Integrate Pricing Service behind FF #5276
base: main
Are you sure you want to change the base?
[PM-16684] Integrate Pricing Service behind FF #5276
Conversation
Many instances of StaticStore use are just to get the ProductTierType of a PlanType, but this can be derived from the PlanType itself without having to fetch the entire plan.
New Issues (17)Checkmarx found the following issues in this Pull Request
Fixed Issues (26)Great job! The following issues were fixed in this Pull Request
|
The tweaks make sense to me -- on to true team review though. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Just left a suggestion
message: $"Request to the Pricing Service failed with status {response.StatusCode}"); | ||
} | ||
|
||
private static async Task<T> DeserializeAsync<T>(HttpContent content) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method could be replaced by ReadFromJsonAsync
in System.Net.Http.Json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great call! Replaced in 8d60635.
return new PlanAdapter(response); | ||
if (response.IsSuccessStatusCode) | ||
{ | ||
var plan = await DeserializeAsync<PlanDTO>(response.Content); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var plan = await DeserializeAsync<PlanDTO>(response.Content); | |
var plan = await response.Content.ReadFromJsonAsync<PlanDTO>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed in 8d60635
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work! Thanks for implementing my suggestion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent writeup, thank you sir
Ephemeral Environment Details: |
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-16684
📔 Objective
The primary goal of this PR is to replace invocations of the
StaticStore.Plans
property andStaticStore.GetPlan
method with invocations of the newly addedPricingClient
.The Pricing Client
The
PricingClient
is a simple wrapper over an HTTP Client that sends requests to the upcoming Bitwarden Pricing Service, an API responsible for managing Bitwarden's plan information.The key lines in the
PricingClient
for the purpose of this PR are:In effect, until the Pricing Service is actually stood up and the feature flag is enabled, we haven't really made any change to the data source we're using to get plan information. Additionally, we can maintain the same exact contract as we had with the
StaticStore
because ultimately, responses from the Pricing Service will be mapped to theStaticStore.Plan
record type which is woven throughout the codebase. We are, however, changing the way retrieving this plan information is managed.Plan Retrieval
There are two major changes to retrieving plans within the platform that are addressed in this PR:
Our retrieval contracts have changed from
PlanType -> Plan
toPlanType -> Task<Plan>
since, at the point the feature flag is enabled, plan retrieval will include a cache request (not implemented yet), a network request or both. This has wide-ranging effects on the platform which consistently fell into a habit of retrieving plans in areas not ideal for data retrieval such as within several layers of nested models. This PR attempts to address this as efficiently as possible with the least potential disruptions to the platform; much of this will be identified as moving plan retrieval up to the closest asynchronous point of execution and then passing thePlan
down through the model chain. It's not perfect, but it's what we've got without a large refactoring of our reliance on heavy and logic-containing models.Because the Pricing Service will be an internally managed Bitwarden service, we will no longer support retrieving plans on Self-Host instances. This is, in my opinion at least, correct. Plans are our own domain construct used to facilitate the purchase of subscriptions and grant specific feature access to our
Organization
entities. On the cloud side, our license generation is dependent on these plans, which is fine because we can retrieve plans on cloud. On the Self-Host side, the organization is created directly from the license itself, so there should be no need to have a detailed understanding of the plan that led to the licenses generation (outside of a few pieces of data for display purposes).Many places that were potential Self-Host usages were just retrieving a
Plan
for the sake of getting theProductTier
. But we don't need to do that asProductTier
can be derived directly fromPlanType
.Considerations & Follow-Ups
Although I tried to remove every
StaticStore
invocation, there are a few spots that remain problematic.The MaxProjectsQuery can be invoked in the Self-Host path and relies on plan information. I opened https://bitwarden.atlassian.net/browse/PM-17122 to address moving the information being garnered from the plan in the query to the
OrganizationLicense
itself.The IsAddOnSubscriptionItem utility in the
StaticStore
is used very deep within nested models in a Billing response. I've opened https://bitwarden.atlassian.net/browse/PM-16844 to move this simplebool
data out of our own domain and into Stripe as metadata attached to ourPrice
entities.In the current platform, we handle an issue where the DLLs running on our users' Self-Host instances can be outdated, leading to new plan types that exist in cloud not existing on the Self-Host instance. We manage this by ensuring the
PlanType
attached to theOrganizationLicense
exists in theStaticStore
running on the user's instance. If it doesn't, we throw an exception. This PR removes that check because Self-Host instances can't invoke the Pricing Service, so this issue will ultimately reappear. I'd argue, however, that this check is unnecessary. Since the user purchased the organization subscription against Cloud, it's valid and the features assigned to the organization should be respected. It doesn't matter whether or not the new plan type exists on the Self-Host instance. The only place I can identify this being an issue is on theUpdateLicenseComponent
where we check theProductTierType
of the organization. However, I think this can be managed by introducing aProductTierType.Unkown
and flipping this check to just assert the organization is Enterprise tier. (Ticket pending for this)Throughout the course of my testing, I also uncovered at least 3 areas that are invokable from a Self-Host instance, but can attempt to update a subscription, which should not be allowed:
📸 Screenshots
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:
) or similar for great changes:memo:
) or ℹ️ (:information_source:
) for notes or general info:question:
) for questions:thinking:
) or 💭 (:thought_balloon:
) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:
) for suggestions / improvements:x:
) or:warning:
) for more significant problems or concerns needing attention:seedling:
) or ♻️ (:recycle:
) for future improvements or indications of technical debt:pick:
) for minor or nitpick changes