-
Notifications
You must be signed in to change notification settings - Fork 28
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
API: Migrate to Plan / Tier Tables #1099
Conversation
Codecov ReportAttention: Patch coverage is
✅ All tests successful. No failed tests found.
Additional details and impacted files@@ Coverage Diff @@
## main #1099 +/- ##
==========================================
- Coverage 96.11% 96.07% -0.04%
==========================================
Files 835 836 +1
Lines 19572 19611 +39
==========================================
+ Hits 18812 18842 +30
- Misses 760 769 +9
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
Codecov ReportAttention: Patch coverage is ✅ All tests successful. No failed tests found.
📢 Thoughts on this report? Let us know! |
❌ 5 Tests Failed:
View the top 3 failed tests by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
✅ All tests successful. No failed tests were found. 📣 Thoughts on this report? Let Codecov know! | Powered by Codecov |
@@ -131,11 +130,6 @@ def validate_value(self, value: str) -> str: | |||
plan["value"] for plan in plan_service.available_plans(current_owner) | |||
] | |||
if value not in plan_values: | |||
if value in SENTRY_PAID_USER_PLAN_REPRESENTATIONS: |
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.
Removed this because it would now just be an extra DB call for a log lol
bef1fa7
to
3a5b3af
Compare
This PR includes changes to |
@@ -219,7 +222,7 @@ def get_plan(self, phase: Dict[str, Any]) -> str: | |||
plan_name = list(stripe_plan_dict.keys())[ |
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 can probably be further cleaned up by doing a get on the stripe_id 🤔
@@ -637,7 +637,10 @@ def validate_upload( | |||
owner = _determine_responsible_owner(repository) | |||
|
|||
# If author is on per repo billing, check their repo credits | |||
if owner.plan not in USER_PLAN_REPRESENTATIONS and owner.repo_credits <= 0: | |||
if ( | |||
owner.plan not in Plan.objects.values_list("name", flat=True) |
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 is some mega legacy code, I don't think anyone is on per repo billing anymore and can probably be deleted tbh
@@ -69,17 +71,24 @@ def __getitem__(self, key): | |||
|
|||
|
|||
class StripeWebhookHandlerTests(APITestCase): | |||
@classmethod | |||
def setUpClass(cls): |
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.
beauty
def resolve_plan_representation(owner: Owner, info: GraphQLResolveInfo) -> PlanData: | ||
info.context["plan_service"] = PlanService(current_org=owner) | ||
free_plan = FREE_PLAN_REPRESENTATIONS[PlanName.BASIC_PLAN_NAME.value] | ||
return free_plan.convert_to_DTO() | ||
free_plan = Plan.objects.select_related("tier").get( |
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.
direct fetch of the basic plan info, will swap to new dev plan when we do that integration
@@ -794,22 +795,32 @@ def update_plan(self, owner, desired_plan): | |||
on current state, might create a stripe checkout session and return | |||
the checkout session's ID, which is a string. Otherwise returns None. | |||
""" | |||
if desired_plan["value"] in FREE_PLAN_REPRESENTATIONS: | |||
try: | |||
plan = Plan.objects.get(name=desired_plan["value"]) |
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 is a bit of a logic update for what I interpreted the other const to be doing; check if the plan exists AND check if the plan is active; otherwise its an old plan we don't want users to transition to anymore
) | ||
return None | ||
|
||
if plan.paid_plan is False: |
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.
a logic "reversal" here because we've already ruled out the non-existent/old plan case. So we can check if the plan is a free plan and set that plan data, otherwise do the paid plan logic
@@ -26,7 +26,7 @@ freezegun | |||
google-cloud-pubsub | |||
gunicorn>=22.0.0 | |||
https://github.com/codecov/opentelem-python/archive/refs/tags/v0.0.4a1.tar.gz#egg=codecovopentelem | |||
https://github.com/codecov/shared/archive/fe16480b3646a616ff412d5c0a28cafd2c7104c1.tar.gz#egg=shared |
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.
I don't see this commit on the branch in shared - can you update to the most recent commit on your branch?
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.
You can find the commit at codecov/shared#479. We need the changes from that version and will update to the new shared version once it's merged. Feel free to review the rest of the files—we did the same for the worker PR
This PR includes changes to |
This PR includes changes to |
api/internal/owner/serializers.py
Outdated
active_plans = list( | ||
Plan.objects.select_related("tier").filter(paid_plan=True, is_active=True) | ||
) | ||
active_plan_names = {plan.name for plan in active_plans} |
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.
active_plan_names = {plan.name for plan in active_plans} | |
active_plan_names = set(active_plans.values_list("name", flat=True)) |
written from memory so there may be typos, but instead of iterating through, just return the values you want
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.
It's cool that django can keep those sets in memory and do additional filters if needed
api/internal/owner/serializers.py
Outdated
plan.name | ||
for plan in active_plans | ||
if plan.tier.tier_name == TierName.TEAM.value | ||
} |
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.
do a filter on the qset to achieve plan.tier.tier_name == TierName.TEAM.value rather than iterating and checking. then do a values_list to get the names
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.
Updated ✔️ Thanks for the suggestion
@@ -184,7 +187,7 @@ def validate(self, plan: Dict[str, Any]) -> Dict[str, Any]: | |||
"Quantity or plan for paid plan must be different from the existing one" | |||
) | |||
if ( | |||
plan["value"] in TEAM_PLAN_REPRESENTATIONS | |||
plan["value"] in team_tier_plans |
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.
I thought that with the new structuring of plans, instead of doing a comparison like this, you would get the Plan
object that plan["value"]
references, and then you could do, if plan.tier.tier_name == TierName.TEAM.value
the plan should have these attributes on it, so you don't need to compile a list of other plans and ask is this one in that list - just check the attribute.
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.
Yep totally on board! Unfortunately the frontend is the thing that sends us these values right now and it's super naive in how it's being done. We could fetch the plan based off the name here maybe and do a similar check, but ultimately its about the same
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.
services/billing.py
Outdated
return False | ||
# If from TEAM to PRO, then considered a similar plan but really is an upgrade | ||
elif owner.plan in TEAM_PLANS and desired_plan["value"] not in TEAM_PLANS: | ||
elif owner.plan in team_plans and desired_plan["value"] not in team_plans: |
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.
same question on these checks - could you not do current_plan_info.tier_name
?
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.
yeah fair point, makes sense
elif ( | ||
current_plan_info.tier.tier_name == TierName.TEAM.value | ||
and desired_plan_info.tier.tier_name != TierName.TEAM.value | ||
): |
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.
_is_extending_term
and _is_similar_plan
- they are both getting Plans from the db, but they are both only called once, from the same place (_get_proration_params
)
could do the db calls in _get_proration_params
, then pass the Plan objs or the relevant fields to _is_extending_term
and _is_similar_plan
to cut db hits in half
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.
approving on behalf of @nora-codecov who is awaiting getting added to this repo
Purpose/Motivation
This PR relies on this version of shared: codecov/shared#479.
The main objective of this PR is to migrate all Backend Service logic to use the new plan and tiers tables instead of the Plan constants. You can read more about milestone 3 here.
A core change introduced in this PR is the creation of mocks for plans and tiers, which are used in most test setup functions. To ensure efficient testing, we use setUpClass to set up these mocks once per test class, avoiding the need to recreate them for each test.
Links to relevant tickets
Closes codecov/engineering-team#3252
What does this PR do?
Include a brief description of the changes in this PR. Bullet points are your friend.
Notes to Reviewer
Anything to note to the team? Any tips on how to review, or where to start?
Legal Boilerplate
Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. In 2022 this entity acquired Codecov and as result Sentry is going to need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.