Skip to content
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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

ajay-sentry
Copy link
Contributor

@ajay-sentry ajay-sentry commented Jan 15, 2025

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.

@ajay-sentry ajay-sentry requested a review from a team as a code owner January 15, 2025 17:40
Copy link

codecov bot commented Jan 15, 2025

Codecov Report

Attention: Patch coverage is 96.82540% with 2 lines in your changes missing coverage. Please review.

Project coverage is 96.10%. Comparing base (9f89174) to head (49f2af8).
Report is 2 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
services/billing.py 88.23% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1099    +/-   ##
========================================
  Coverage   96.09%   96.10%            
========================================
  Files         832      835     +3     
  Lines       19507    19628   +121     
========================================
+ Hits        18746    18863   +117     
- Misses        761      765     +4     
Flag Coverage Δ
unit 96.00% <96.82%> (-0.01%) ⬇️
unit-latest-uploader 96.00% <96.82%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

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

@codecov-notifications
Copy link

codecov-notifications bot commented Jan 15, 2025

Codecov Report

Attention: Patch coverage is 96.82540% with 2 lines in your changes missing coverage. Please review.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
services/billing.py 88.23% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codecov-qa
Copy link

codecov-qa bot commented Jan 15, 2025

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
2710 5 2705 7
View the top 3 failed tests by shortest run time
graphql_api/tests/test_owner.py::TestOwnerType::test_fetch_available_plans_is_enterprise_plan
Stack Traces | 0.442s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_fetch_available_plans_is_enterprise_plan>

    def test_fetch_available_plans_is_enterprise_plan(self):
        current_org = OwnerFactory(
            username="random-plan-user",
            service="github",
            plan=PlanName.FREE_PLAN_NAME.value,
        )
    
        query = """{
            owner(username: "%s") {
                availablePlans {
                    value
                    isEnterprisePlan
                    isProPlan
                    isTeamPlan
                    isSentryPlan
                    isFreePlan
                    isTrialPlan
                }
            }
        }
        """ % (current_org.username)
        data = self.gql_request(query, owner=current_org)
>       assert data == {
            "owner": {
                "availablePlans": [
                    {
                        "value": "users-basic",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": True,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-free",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": True,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-pr-inappm",
                        "isEnterprisePlan": False,
                        "isProPlan": True,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-pr-inappy",
                        "isEnterprisePlan": False,
                        "isProPlan": True,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-teamm",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": True,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-teamy",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": True,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                ]
            }
        }
E       AssertionError: assert {'owner': {'a...Plans': None}} == {'owner': {'a...False, ...}]}}
E         
E         Differing items:
E         {'owner': {'availablePlans': None}} != {'owner': {'availablePlans': [{'isEnterprisePlan': False, 'isFreePlan': True, 'isProPlan': False, 'isSentryPlan': Fals...Plan': False, ...}, {'isEnterprisePlan': False, 'isFreePlan': False, 'isProPlan': False, 'isSentryPlan': False, ...}]}}
E         Use -v to get more diff

graphql_api/tests/test_owner.py:1151: AssertionError
graphql_api/tests/test_owner.py::TestOwnerType::test_resolve_number_of_uploads_per_user
Stack Traces | 0.625s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_resolve_number_of_uploads_per_user>

    def test_resolve_number_of_uploads_per_user(self):
        query_uploads_number = """{
            owner(username: "%s") {
               numberOfUploads
            }
        }
        """
        repository = RepositoryFactory.create(
            author__plan=PlanName.BASIC_PLAN_NAME.value, author=self.owner
        )
        first_commit = CommitFactory.create(repository=repository)
        first_report = CommitReportFactory.create(
            commit=first_commit, report_type=ReportType.COVERAGE.value
        )
        for i in range(150):
            upload = UploadFactory.create(report=first_report)
            insert_coverage_measurement(
                owner_id=self.owner.ownerid,
                repo_id=repository.repoid,
                commit_id=first_commit.id,
                upload_id=upload.id,
                uploader_used=UploaderType.CLI.value,
                private_repo=repository.private,
                report_type=first_report.report_type,
            )
        query = query_uploads_number % (repository.author.username)
        data = self.gql_request(query, owner=self.owner)
>       assert data["owner"]["numberOfUploads"] == 150
E       assert None == 150

graphql_api/tests/test_owner.py:383: AssertionError
graphql_api/tests/test_owner.py::TestOwnerType::test_owner_plan_status
Stack Traces | 1.22s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_owner_plan_status>

    @freeze_time("2023-06-19")
    def test_owner_plan_status(self):
        current_org = OwnerFactory(
            username="random-plan-user",
            service="github",
            trial_start_date=timezone.now(),
            trial_end_date=timezone.now() + timedelta(days=14),
            trial_status=TrialStatus.ONGOING.value,
        )
        query = """{
            owner(username: "%s") {
                plan {
                    trialStatus
                }
            }
        }
        """ % (current_org.username)
        data = self.gql_request(query, owner=current_org)
>       assert data["owner"]["plan"] == {
            "trialStatus": "ONGOING",
        }
E       AssertionError: assert None == {'trialStatus': 'ONGOING'}

graphql_api/tests/test_owner.py:644: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Copy link

codecov-public-qa bot commented Jan 15, 2025

❌ 5 Tests Failed:

Tests completed Failed Passed Skipped
2711 5 2706 6
View the top 3 failed tests by shortest run time
graphql_api/tests/test_owner.py::TestOwnerType::test_fetch_available_plans_is_enterprise_plan
Stack Traces | 0.442s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_fetch_available_plans_is_enterprise_plan>

    def test_fetch_available_plans_is_enterprise_plan(self):
        current_org = OwnerFactory(
            username="random-plan-user",
            service="github",
            plan=PlanName.FREE_PLAN_NAME.value,
        )
    
        query = """{
            owner(username: "%s") {
                availablePlans {
                    value
                    isEnterprisePlan
                    isProPlan
                    isTeamPlan
                    isSentryPlan
                    isFreePlan
                    isTrialPlan
                }
            }
        }
        """ % (current_org.username)
        data = self.gql_request(query, owner=current_org)
>       assert data == {
            "owner": {
                "availablePlans": [
                    {
                        "value": "users-basic",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": True,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-free",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": True,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-pr-inappm",
                        "isEnterprisePlan": False,
                        "isProPlan": True,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-pr-inappy",
                        "isEnterprisePlan": False,
                        "isProPlan": True,
                        "isTeamPlan": False,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-teamm",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": True,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                    {
                        "value": "users-teamy",
                        "isEnterprisePlan": False,
                        "isProPlan": False,
                        "isTeamPlan": True,
                        "isSentryPlan": False,
                        "isFreePlan": False,
                        "isTrialPlan": False,
                    },
                ]
            }
        }
E       AssertionError: assert {'owner': {'a...Plans': None}} == {'owner': {'a...False, ...}]}}
E         
E         Differing items:
E         {'owner': {'availablePlans': None}} != {'owner': {'availablePlans': [{'isEnterprisePlan': False, 'isFreePlan': True, 'isProPlan': False, 'isSentryPlan': Fals...Plan': False, ...}, {'isEnterprisePlan': False, 'isFreePlan': False, 'isProPlan': False, 'isSentryPlan': False, ...}]}}
E         Use -v to get more diff

graphql_api/tests/test_owner.py:1151: AssertionError
graphql_api/tests/test_owner.py::TestOwnerType::test_resolve_number_of_uploads_per_user
Stack Traces | 0.625s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_resolve_number_of_uploads_per_user>

    def test_resolve_number_of_uploads_per_user(self):
        query_uploads_number = """{
            owner(username: "%s") {
               numberOfUploads
            }
        }
        """
        repository = RepositoryFactory.create(
            author__plan=PlanName.BASIC_PLAN_NAME.value, author=self.owner
        )
        first_commit = CommitFactory.create(repository=repository)
        first_report = CommitReportFactory.create(
            commit=first_commit, report_type=ReportType.COVERAGE.value
        )
        for i in range(150):
            upload = UploadFactory.create(report=first_report)
            insert_coverage_measurement(
                owner_id=self.owner.ownerid,
                repo_id=repository.repoid,
                commit_id=first_commit.id,
                upload_id=upload.id,
                uploader_used=UploaderType.CLI.value,
                private_repo=repository.private,
                report_type=first_report.report_type,
            )
        query = query_uploads_number % (repository.author.username)
        data = self.gql_request(query, owner=self.owner)
>       assert data["owner"]["numberOfUploads"] == 150
E       assert None == 150

graphql_api/tests/test_owner.py:383: AssertionError
graphql_api/tests/test_owner.py::TestOwnerType::test_owner_plan_status
Stack Traces | 1.22s run time
self = <graphql_api.tests.test_owner.TestOwnerType testMethod=test_owner_plan_status>

    @freeze_time("2023-06-19")
    def test_owner_plan_status(self):
        current_org = OwnerFactory(
            username="random-plan-user",
            service="github",
            trial_start_date=timezone.now(),
            trial_end_date=timezone.now() + timedelta(days=14),
            trial_status=TrialStatus.ONGOING.value,
        )
        query = """{
            owner(username: "%s") {
                plan {
                    trialStatus
                }
            }
        }
        """ % (current_org.username)
        data = self.gql_request(query, owner=current_org)
>       assert data["owner"]["plan"] == {
            "trialStatus": "ONGOING",
        }
E       AssertionError: assert None == {'trialStatus': 'ONGOING'}

graphql_api/tests/test_owner.py:644: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📢 Thoughts on this report? Let us know!

Copy link
Contributor

github-actions bot commented Jan 15, 2025

✅ All tests successful. No failed tests were found.

📣 Thoughts on this report? Let Codecov know! | Powered by Codecov

@ajay-sentry ajay-sentry requested a review from a team as a code owner January 15, 2025 20:02
@@ -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:
Copy link
Contributor Author

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

billing/helpers.py Outdated Show resolved Hide resolved
@RulaKhaled RulaKhaled force-pushed the Ajay/milestone-3-migration branch from bef1fa7 to 3a5b3af Compare January 20, 2025 11:51
Copy link
Contributor

github-actions bot commented Jan 21, 2025

This PR includes changes to shared. Please review them here: codecov/shared@fe16480...8277f6d

@ajay-sentry ajay-sentry changed the title [WIP] Plan Milestone 3 Transitions API: Migrate to Plan / Tier Tables Jan 23, 2025
@@ -219,7 +222,7 @@ def get_plan(self, phase: Dict[str, Any]) -> str:
plan_name = list(stripe_plan_dict.keys())[
Copy link
Contributor Author

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)
Copy link
Contributor Author

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):
Copy link
Contributor

Choose a reason for hiding this comment

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

beauty

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.

Migrate plan service logic in API
2 participants