diff --git a/api/internal/owner/serializers.py b/api/internal/owner/serializers.py index 73aef837cf..592fa6aeeb 100644 --- a/api/internal/owner/serializers.py +++ b/api/internal/owner/serializers.py @@ -12,9 +12,9 @@ TEAM_PLAN_MAX_USERS, TEAM_PLAN_REPRESENTATIONS, ) +from shared.plan.service import PlanService from codecov_auth.models import Owner -from plan.service import PlanService from services.billing import BillingService from services.sentry import send_user_webhook as send_sentry_webhook diff --git a/billing/tests/test_views.py b/billing/tests/test_views.py index 5b02a2e52f..293f3d4048 100644 --- a/billing/tests/test_views.py +++ b/billing/tests/test_views.py @@ -571,7 +571,7 @@ def test_customer_subscription_created_sets_plan_info(self): assert self.owner.plan == plan_name @freeze_time("2023-06-19") - @patch("plan.service.PlanService.expire_trial_when_upgrading") + @patch("shared.plan.service.PlanService.expire_trial_when_upgrading") @patch("services.billing.stripe.PaymentMethod.attach") @patch("services.billing.stripe.Customer.modify") def test_customer_subscription_created_can_trigger_trial_expiration( diff --git a/billing/views.py b/billing/views.py index 6c8229831e..ec1de279c1 100644 --- a/billing/views.py +++ b/billing/views.py @@ -10,9 +10,9 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView +from shared.plan.service import PlanService from codecov_auth.models import Owner -from plan.service import PlanService from services.task.task import TaskService from .constants import StripeHTTPHeaders, StripeWebhookEvents diff --git a/codecov_auth/admin.py b/codecov_auth/admin.py index 7dcf884418..bb717ac8d6 100644 --- a/codecov_auth/admin.py +++ b/codecov_auth/admin.py @@ -20,13 +20,13 @@ StripeBilling, ) from shared.plan.constants import USER_PLAN_REPRESENTATIONS +from shared.plan.service import PlanService from codecov.admin import AdminMixin from codecov.commands.exceptions import ValidationError from codecov_auth.helpers import History from codecov_auth.models import OrganizationLevelToken, Owner, SentryUser, Session, User from codecov_auth.services.org_level_token_service import OrgLevelTokenService -from plan.service import PlanService from services.task import TaskService from utils.services import get_short_service_name diff --git a/codecov_auth/commands/owner/interactors/cancel_trial.py b/codecov_auth/commands/owner/interactors/cancel_trial.py index 714e10d185..556234b2a0 100644 --- a/codecov_auth/commands/owner/interactors/cancel_trial.py +++ b/codecov_auth/commands/owner/interactors/cancel_trial.py @@ -1,9 +1,10 @@ +from shared.plan.service import PlanService + from codecov.commands.base import BaseInteractor from codecov.commands.exceptions import Unauthorized, ValidationError from codecov.db import sync_to_async from codecov_auth.helpers import current_user_part_of_org from codecov_auth.models import Owner -from plan.service import PlanService class CancelTrialInteractor(BaseInteractor): diff --git a/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py b/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py index a18b337e97..a0027f9d7e 100644 --- a/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py +++ b/codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py @@ -1,9 +1,9 @@ +from shared.plan.service import PlanService from shared.upload.utils import query_monthly_coverage_measurements from codecov.commands.base import BaseInteractor from codecov.db import sync_to_async from codecov_auth.models import Owner -from plan.service import PlanService from services.redis_configuration import get_redis_connection redis = get_redis_connection() diff --git a/codecov_auth/commands/owner/interactors/start_trial.py b/codecov_auth/commands/owner/interactors/start_trial.py index 3058f230cc..c14f2c6229 100644 --- a/codecov_auth/commands/owner/interactors/start_trial.py +++ b/codecov_auth/commands/owner/interactors/start_trial.py @@ -1,9 +1,10 @@ +from shared.plan.service import PlanService + from codecov.commands.base import BaseInteractor from codecov.commands.exceptions import Unauthorized, ValidationError from codecov.db import sync_to_async from codecov_auth.helpers import current_user_part_of_org from codecov_auth.models import Owner -from plan.service import PlanService class StartTrialInteractor(BaseInteractor): diff --git a/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py b/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py index ac9adae6c5..138b04f64e 100644 --- a/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py +++ b/codecov_auth/commands/owner/interactors/tests/test_cancel_trial.py @@ -4,10 +4,12 @@ from asgiref.sync import async_to_sync from django.test import TransactionTestCase from freezegun import freeze_time +from shared.django_apps.codecov.commands.exceptions import ValidationError from shared.django_apps.core.tests.factories import OwnerFactory from shared.plan.constants import PlanName, TrialStatus -from codecov.commands.exceptions import Unauthorized, ValidationError +from codecov.commands.exceptions import Unauthorized +from codecov.commands.exceptions import ValidationError as CodecovValidationError from codecov_auth.models import Owner from ..cancel_trial import CancelTrialInteractor @@ -26,7 +28,7 @@ def test_cancel_trial_raises_exception_when_owner_is_not_in_db(self): username="random-user-123", service="github", ) - with pytest.raises(ValidationError): + with pytest.raises(CodecovValidationError): self.execute(current_user=current_user, org_username="some-other-username") def test_cancel_trial_raises_exception_when_current_user_not_part_of_org(self): diff --git a/codecov_auth/commands/owner/interactors/tests/test_start_trial.py b/codecov_auth/commands/owner/interactors/tests/test_start_trial.py index da9b3469cd..3504e9d527 100644 --- a/codecov_auth/commands/owner/interactors/tests/test_start_trial.py +++ b/codecov_auth/commands/owner/interactors/tests/test_start_trial.py @@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync from django.test import TransactionTestCase from freezegun import freeze_time +from shared.django_apps.codecov.commands.exceptions import ValidationError from shared.django_apps.core.tests.factories import OwnerFactory from shared.plan.constants import ( TRIAL_PLAN_SEATS, @@ -12,7 +13,8 @@ TrialStatus, ) -from codecov.commands.exceptions import Unauthorized, ValidationError +from codecov.commands.exceptions import Unauthorized +from codecov.commands.exceptions import ValidationError as CodecovValidationError from codecov_auth.models import Owner from ..start_trial import StartTrialInteractor @@ -31,7 +33,7 @@ def test_start_trial_raises_exception_when_owner_is_not_in_db(self): username="random-user-123", service="github", ) - with pytest.raises(ValidationError): + with pytest.raises(CodecovValidationError): self.execute(current_user=current_user, org_username="some-other-username") def test_cancel_trial_raises_exception_when_current_user_not_part_of_org(self): diff --git a/codecov_auth/tests/test_admin.py b/codecov_auth/tests/test_admin.py index 524319a0fe..953752c907 100644 --- a/codecov_auth/tests/test_admin.py +++ b/codecov_auth/tests/test_admin.py @@ -300,7 +300,7 @@ def test_start_trial_ui_display(self): assert res.status_code == 200 assert "Extending trial for:" in str(res.content) - @patch("plan.service.PlanService.start_trial_manually") + @patch("shared.plan.service.PlanService.start_trial_manually") def test_start_trial_action(self, mock_start_trial_service): mock_start_trial_service.return_value = None org_to_be_trialed = OwnerFactory() @@ -317,7 +317,7 @@ def test_start_trial_action(self, mock_start_trial_service): assert res.status_code == 302 assert mock_start_trial_service.called - @patch("plan.service.PlanService._start_trial_helper") + @patch("shared.plan.service.PlanService._start_trial_helper") def test_extend_trial_action(self, mock_start_trial_service): mock_start_trial_service.return_value = None org_to_be_trialed = OwnerFactory() @@ -337,7 +337,7 @@ def test_extend_trial_action(self, mock_start_trial_service): assert mock_start_trial_service.called assert mock_start_trial_service.call_args.kwargs == {"is_extension": True} - @patch("plan.service.PlanService.start_trial_manually") + @patch("shared.plan.service.PlanService.start_trial_manually") def test_start_trial_paid_plan(self, mock_start_trial_service): mock_start_trial_service.side_effect = ValidationError( "Cannot trial from a paid plan" diff --git a/graphql_api/tests/test_plan.py b/graphql_api/tests/test_plan.py index 16011ff6aa..ef9df4e389 100644 --- a/graphql_api/tests/test_plan.py +++ b/graphql_api/tests/test_plan.py @@ -136,7 +136,7 @@ def test_owner_plan_data_has_seats_left(self): data = self.gql_request(query, owner=current_org) assert data["owner"]["plan"] == {"hasSeatsLeft": True} - @patch("services.self_hosted.get_current_license") + @patch("shared.self_hosted.service.get_current_license") def test_plan_user_count_for_enterprise_org(self, mocked_license): """ If an Org has an enterprise license, number_allowed_users from their license @@ -190,10 +190,11 @@ def test_plan_user_count_for_enterprise_org(self, mocked_license): } """ % (enterprise_org.username) data = self.gql_request(query, owner=enterprise_org) + print(data, "look here 1") assert data["owner"]["plan"]["planUserCount"] == 5 assert data["owner"]["plan"]["hasSeatsLeft"] == False - @patch("services.self_hosted.get_current_license") + @patch("shared.self_hosted.service.get_current_license") def test_plan_user_count_for_enterprise_org_invaild_license(self, mocked_license): mock_enterprise_license = LicenseInformation( is_valid=False, diff --git a/graphql_api/types/owner/owner.py b/graphql_api/types/owner/owner.py index 9860300007..b6314463f5 100644 --- a/graphql_api/types/owner/owner.py +++ b/graphql_api/types/owner/owner.py @@ -9,6 +9,7 @@ from django.conf import settings from graphql import GraphQLResolveInfo from shared.plan.constants import FREE_PLAN_REPRESENTATIONS, PlanData, PlanName +from shared.plan.service import PlanService import services.activation as activation import timeseries.helpers as timeseries_helpers @@ -40,7 +41,6 @@ from graphql_api.types.enums import OrderingDirection, RepositoryOrdering from graphql_api.types.errors.errors import NotFoundError from graphql_api.types.repository.repository import TOKEN_UNAVAILABLE -from plan.service import PlanService from services.billing import BillingService from services.profiling import ProfilingSummary from services.redis_configuration import get_redis_connection diff --git a/graphql_api/types/plan/plan.py b/graphql_api/types/plan/plan.py index 8e578461ac..189613f81e 100644 --- a/graphql_api/types/plan/plan.py +++ b/graphql_api/types/plan/plan.py @@ -5,10 +5,10 @@ from shared.plan.constants import ( TrialStatus, ) +from shared.plan.service import PlanService from codecov.db import sync_to_async from graphql_api.helpers.ariadne import ariadne_load_local_graphql -from plan.service import PlanService plan = ariadne_load_local_graphql(__file__, "plan.graphql") plan_bindable = ObjectType("Plan") diff --git a/graphql_api/types/plan_representation/plan_representation.py b/graphql_api/types/plan_representation/plan_representation.py index a0eef20ff0..c7b9c6fb95 100644 --- a/graphql_api/types/plan_representation/plan_representation.py +++ b/graphql_api/types/plan_representation/plan_representation.py @@ -2,9 +2,9 @@ from ariadne import ObjectType from shared.plan.constants import PlanData +from shared.plan.service import PlanService from graphql_api.helpers.ariadne import ariadne_load_local_graphql -from plan.service import PlanService plan_representation = ariadne_load_local_graphql( __file__, "plan_representation.graphql" diff --git a/plan/__init__.py b/plan/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plan/service.py b/plan/service.py deleted file mode 100644 index 5ad8658ba0..0000000000 --- a/plan/service.py +++ /dev/null @@ -1,303 +0,0 @@ -import logging -from datetime import datetime, timedelta -from typing import List, Optional - -from shared.plan.constants import ( - BASIC_PLAN, - FREE_PLAN, - FREE_PLAN_REPRESENTATIONS, - PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS, - SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - TEAM_PLAN_MAX_USERS, - TEAM_PLAN_REPRESENTATIONS, - TRIAL_PLAN_REPRESENTATION, - TRIAL_PLAN_SEATS, - USER_PLAN_REPRESENTATIONS, - PlanData, - PlanName, - TrialDaysAmount, - TrialStatus, -) - -from codecov.commands.exceptions import ValidationError -from codecov_auth.models import Owner -from services import sentry -from services.self_hosted import enterprise_has_seats_left, license_seats -from utils.config import get_config - -log = logging.getLogger(__name__) - - -# TODO: Consider moving some of these methods to the billing directory as they overlap billing functionality -class PlanService: - def __init__(self, current_org: Owner): - """ - Initializes a plan service object with a plan. The plan will be a trial plan - if applicable - - Args: - current_org (Owner): this is selected organization entry. This is not the user that is sending the request. - - Returns: - No value - """ - self.current_org = current_org - if self.current_org.plan not in USER_PLAN_REPRESENTATIONS: - raise ValueError("Unsupported plan") - self._plan_data = None - - def update_plan(self, name, user_count: int | None) -> None: - if name not in USER_PLAN_REPRESENTATIONS: - raise ValueError("Unsupported plan") - if not user_count: - raise ValueError("Quantity Needed") - self.current_org.plan = name - self.current_org.plan_user_count = user_count - self._plan_data = USER_PLAN_REPRESENTATIONS[self.current_org.plan] - self.current_org.delinquent = False - self.current_org.save() - - def current_org(self) -> Owner: - return self.current_org - - def set_default_plan_data(self) -> None: - log.info(f"Setting plan to users-basic for owner {self.current_org.ownerid}") - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.plan_activated_users = None - self.current_org.plan_user_count = 1 - self.current_org.stripe_subscription_id = None - self.current_org.save() - - @property - def has_account(self) -> bool: - return False if self.current_org.account is None else True - - @property - def plan_data(self) -> PlanData: - if self._plan_data is not None: - return self._plan_data - - if self.has_account: - self._plan_data = USER_PLAN_REPRESENTATIONS[self.current_org.account.plan] - else: - self._plan_data = USER_PLAN_REPRESENTATIONS[self.current_org.plan] - return self._plan_data - - @plan_data.setter - def set_plan_data(self, plan_data: PlanData | None) -> None: - self._plan_data = plan_data - - @property - def plan_name(self) -> str: - return self.plan_data.value - - @property - def plan_user_count(self) -> int: - if get_config("setup", "enterprise_license"): - return license_seats() - if self.has_account: - return self.current_org.account.total_seat_count - return self.current_org.plan_user_count - - @property - def plan_activated_users(self) -> Optional[List[int]]: - return self.current_org.plan_activated_users - - @property - def pretrial_users_count(self) -> int: - return self.current_org.pretrial_users_count or 1 - - @property - def marketing_name(self) -> str: - return self.plan_data.marketing_name - - @property - def billing_rate(self) -> Optional[str]: - return self.plan_data.billing_rate - - @property - def base_unit_price(self) -> int: - return self.plan_data.base_unit_price - - @property - def benefits(self) -> List[str]: - return self.plan_data.benefits - - @property - def monthly_uploads_limit(self) -> Optional[int]: - """ - Property that returns monthly uploads limit based on your trial status - - Returns: - Optional number of monthly uploads - """ - return self.plan_data.monthly_uploads_limit - - @property - def tier_name(self) -> str: - return self.plan_data.tier_name - - def available_plans(self, owner: Owner) -> List[PlanData]: - """ - Returns the available plans for an owner and an organization - - Args: - current_owner (Owner): this is the user that is sending the request. - - Returns: - No value - """ - available_plans = [] - available_plans.append(BASIC_PLAN) - - if self.plan_name == FREE_PLAN.value: - available_plans.append(FREE_PLAN) - - available_plans += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - - if owner and sentry.is_sentry_user(owner=owner): - available_plans += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - - # If number of activated users is less than or equal to TEAM_PLAN_MAX_USERS - if ( - self.plan_activated_users is None - or len(self.plan_activated_users) <= TEAM_PLAN_MAX_USERS - ): - available_plans += TEAM_PLAN_REPRESENTATIONS.values() - - return available_plans - - def _start_trial_helper( - self, - current_owner: Owner, - end_date: Optional[datetime] = None, - is_extension: bool = False, - ) -> None: - start_date = datetime.now() - - # When they are not extending a trial, have to setup all the default values - if not is_extension: - self.current_org.trial_start_date = start_date - self.current_org.trial_status = TrialStatus.ONGOING.value - self.current_org.plan = PlanName.TRIAL_PLAN_NAME.value - self.current_org.pretrial_users_count = self.current_org.plan_user_count - self.current_org.plan_user_count = TRIAL_PLAN_SEATS - self.current_org.plan_auto_activate = True - - if end_date is None: - self.current_org.trial_end_date = start_date + timedelta( - days=TrialDaysAmount.CODECOV_SENTRY.value - ) - else: - self.current_org.trial_end_date = end_date - self.current_org.trial_fired_by = current_owner.ownerid - self.current_org.save() - - # Trial Data - def start_trial(self, current_owner: Owner) -> None: - """ - Method that starts trial on an organization if the trial_start_date - is not empty. - - Returns: - No value - - Raises: - ValidationError: if trial has already started - """ - if self.trial_status != TrialStatus.NOT_STARTED.value: - raise ValidationError("Cannot start an existing trial") - if self.plan_name not in FREE_PLAN_REPRESENTATIONS: - raise ValidationError("Cannot trial from a paid plan") - - self._start_trial_helper(current_owner) - - def start_trial_manually(self, current_owner: Owner, end_date: datetime) -> None: - """ - Method that start trial immediately and ends at a predefined date for an organization - Used by administrators to manually start and extend trials - - Returns: - No value - """ - # Start a new trial plan for free users currently not on trial - if self.plan_name in FREE_PLAN_REPRESENTATIONS: - self._start_trial_helper(current_owner, end_date, is_extension=False) - # Extend an existing trial plan for users currently on trial - elif self.plan_name in TRIAL_PLAN_REPRESENTATION: - self._start_trial_helper(current_owner, end_date, is_extension=True) - # Paying users cannot start a trial - else: - raise ValidationError("Cannot trial from a paid plan") - - def cancel_trial(self) -> None: - if not self.is_org_trialing: - raise ValidationError("Cannot cancel a trial that is not ongoing") - now = datetime.now() - self.current_org.trial_status = TrialStatus.EXPIRED.value - self.current_org.trial_end_date = now - self.set_default_plan_data() - - def expire_trial_when_upgrading(self) -> None: - """ - Method that expires trial on an organization based on it's current trial status. - - - Returns: - No value - """ - if self.trial_status == TrialStatus.EXPIRED.value: - return - if self.trial_status != TrialStatus.CANNOT_TRIAL.value: - # Not adjusting the trial start/end dates here as some customers can - # directly purchase a plan without trialing first - self.current_org.trial_status = TrialStatus.EXPIRED.value - self.current_org.plan_activated_users = None - self.current_org.plan_user_count = ( - self.current_org.pretrial_users_count or 1 - ) - self.current_org.trial_end_date = datetime.now() - - self.current_org.save() - - @property - def trial_status(self) -> TrialStatus: - return self.current_org.trial_status - - @property - def trial_start_date(self) -> Optional[datetime]: - return self.current_org.trial_start_date - - @property - def trial_end_date(self) -> Optional[datetime]: - return self.current_org.trial_end_date - - @property - def trial_total_days(self) -> Optional[int]: - return self.plan_data.trial_days - - @property - def is_org_trialing(self) -> bool: - return ( - self.trial_status == TrialStatus.ONGOING.value - and self.plan_name == PlanName.TRIAL_PLAN_NAME.value - ) - - @property - def has_trial_dates(self) -> bool: - return bool(self.trial_start_date and self.trial_end_date) - - @property - def has_seats_left(self) -> bool: - if get_config("setup", "enterprise_license"): - return enterprise_has_seats_left() - if self.has_account: - # edge case: IF the User is already a plan_activated_user on any of the Orgs in the Account, - # AND their Account is at capacity, - # AND they try to become a plan_activated_user on another Org in the Account, - # has_seats_left will evaluate as False even though the User should be allowed to activate on the Org. - return self.current_org.account.can_activate_user() - return ( - self.plan_activated_users is None - or len(self.plan_activated_users) < self.plan_user_count - ) diff --git a/plan/tests/__init__.py b/plan/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plan/tests/test_plan.py b/plan/tests/test_plan.py deleted file mode 100644 index fe046203c6..0000000000 --- a/plan/tests/test_plan.py +++ /dev/null @@ -1,892 +0,0 @@ -from datetime import datetime, timedelta -from unittest.mock import patch - -from django.test import TestCase -from freezegun import freeze_time -from pytest import raises -from shared.django_apps.codecov_auth.tests.factories import ( - AccountFactory, - AccountsUsersFactory, - OwnerFactory, -) -from shared.plan.constants import ( - BASIC_PLAN, - FREE_PLAN, - FREE_PLAN_REPRESENTATIONS, - PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS, - SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - TEAM_PLAN_REPRESENTATIONS, - TRIAL_PLAN_REPRESENTATION, - TRIAL_PLAN_SEATS, - PlanName, - TrialDaysAmount, - TrialStatus, -) - -from codecov.commands.exceptions import ValidationError -from plan.service import PlanService - - -@freeze_time("2023-06-19") -class PlanServiceTests(TestCase): - def test_plan_service_trial_status_not_started(self): - current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) - plan_service = PlanService(current_org=current_org) - - assert plan_service.trial_status == TrialStatus.NOT_STARTED.value - - def test_plan_service_trial_status_expired(self): - trial_start_date = datetime.now() - trial_end_date_expired = trial_start_date - timedelta(days=1) - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date_expired, - trial_status=TrialStatus.EXPIRED.value, - ) - plan_service = PlanService(current_org=current_org) - - assert plan_service.trial_status == TrialStatus.EXPIRED.value - - def test_plan_service_trial_status_ongoing(self): - trial_start_date = datetime.now() - trial_end_date_ongoing = trial_start_date + timedelta(days=5) - current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date_ongoing, - trial_status=TrialStatus.ONGOING.value, - ) - plan_service = PlanService(current_org=current_org) - - assert plan_service.trial_status == TrialStatus.ONGOING.value - assert plan_service.is_org_trialing == True - - def test_plan_service_expire_trial_when_upgrading_successful_if_trial_is_not_started( - self, - ): - current_org_with_ongoing_trial = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=None, - trial_end_date=None, - trial_status=TrialStatus.NOT_STARTED.value, - ) - plan_service = PlanService(current_org=current_org_with_ongoing_trial) - plan_service.expire_trial_when_upgrading() - assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value - assert current_org_with_ongoing_trial.plan_activated_users is None - assert current_org_with_ongoing_trial.plan_user_count == 1 - assert current_org_with_ongoing_trial.trial_end_date == datetime.now() - - def test_plan_service_expire_trial_when_upgrading_successful_if_trial_is_ongoing( - self, - ): - trial_start_date = datetime.now() - trial_end_date_ongoing = trial_start_date + timedelta(days=5) - current_org_with_ongoing_trial = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date_ongoing, - trial_status=TrialStatus.ONGOING.value, - ) - plan_service = PlanService(current_org=current_org_with_ongoing_trial) - plan_service.expire_trial_when_upgrading() - assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value - assert current_org_with_ongoing_trial.plan_activated_users is None - assert current_org_with_ongoing_trial.plan_user_count == 1 - assert current_org_with_ongoing_trial.trial_end_date == datetime.now() - - def test_plan_service_expire_trial_users_pretrial_users_count_if_existing( - self, - ): - trial_start_date = datetime.now() - trial_end_date_ongoing = trial_start_date + timedelta(days=5) - pretrial_users_count = 5 - current_org_with_ongoing_trial = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date_ongoing, - trial_status=TrialStatus.ONGOING.value, - pretrial_users_count=pretrial_users_count, - ) - plan_service = PlanService(current_org=current_org_with_ongoing_trial) - plan_service.expire_trial_when_upgrading() - assert current_org_with_ongoing_trial.trial_status == TrialStatus.EXPIRED.value - assert current_org_with_ongoing_trial.plan_activated_users is None - assert current_org_with_ongoing_trial.plan_user_count == pretrial_users_count - assert current_org_with_ongoing_trial.trial_end_date == datetime.now() - - def test_plan_service_start_trial_errors_if_status_is_ongoing(self): - trial_start_date = datetime.now() - trial_end_date = trial_start_date + timedelta( - days=TrialDaysAmount.CODECOV_SENTRY.value - ) - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - trial_status=TrialStatus.ONGOING.value, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - with self.assertRaises(ValidationError): - plan_service.start_trial(current_owner=current_owner) - - def test_plan_service_start_trial_errors_if_status_is_expired(self): - trial_start_date = datetime.now() - trial_end_date = trial_start_date + timedelta(days=-1) - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - trial_status=TrialStatus.EXPIRED.value, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - with self.assertRaises(ValidationError): - plan_service.start_trial(current_owner=current_owner) - - def test_plan_service_start_trial_errors_if_status_is_cannot_trial(self): - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=None, - trial_end_date=None, - trial_status=TrialStatus.CANNOT_TRIAL.value, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - with self.assertRaises(ValidationError): - plan_service.start_trial(current_owner=current_owner) - - def test_plan_service_start_trial_errors_owners_plan_is_not_a_free_plan(self): - current_org = OwnerFactory( - plan=PlanName.CODECOV_PRO_MONTHLY.value, - trial_start_date=None, - trial_end_date=None, - trial_status=TrialStatus.CANNOT_TRIAL.value, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - with self.assertRaises(ValidationError): - plan_service.start_trial(current_owner=current_owner) - - def test_plan_service_start_trial_succeeds_if_trial_has_not_started(self): - trial_start_date = None - trial_end_date = None - plan_user_count = 5 - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - trial_status=TrialStatus.NOT_STARTED.value, - plan_user_count=plan_user_count, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - plan_service.start_trial(current_owner=current_owner) - assert current_org.trial_start_date == datetime.now() - assert current_org.trial_end_date == datetime.now() + timedelta( - days=TrialDaysAmount.CODECOV_SENTRY.value - ) - assert current_org.trial_status == TrialStatus.ONGOING.value - assert current_org.plan == PlanName.TRIAL_PLAN_NAME.value - assert current_org.pretrial_users_count == plan_user_count - assert current_org.plan_user_count == TRIAL_PLAN_SEATS - assert current_org.plan_auto_activate == True - assert current_org.trial_fired_by == current_owner.ownerid - - def test_plan_service_start_trial_manually(self): - trial_start_date = None - trial_end_date = None - plan_user_count = 5 - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - trial_status=TrialStatus.NOT_STARTED.value, - plan_user_count=plan_user_count, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - plan_service.start_trial_manually( - current_owner=current_owner, end_date="2024-01-01 00:00:00" - ) - assert current_org.trial_start_date == datetime.now() - assert current_org.trial_end_date == "2024-01-01 00:00:00" - assert current_org.trial_status == TrialStatus.ONGOING.value - assert current_org.plan == PlanName.TRIAL_PLAN_NAME.value - assert current_org.pretrial_users_count == plan_user_count - assert current_org.plan_user_count == TRIAL_PLAN_SEATS - assert current_org.plan_auto_activate == True - assert current_org.trial_fired_by == current_owner.ownerid - - def test_plan_service_start_trial_manually_already_on_paid_plan(self): - current_org = OwnerFactory( - plan=PlanName.CODECOV_PRO_MONTHLY.value, - trial_start_date=None, - trial_end_date=None, - trial_status=TrialStatus.NOT_STARTED.value, - ) - plan_service = PlanService(current_org=current_org) - current_owner = OwnerFactory() - - with self.assertRaises(ValidationError): - plan_service.start_trial_manually( - current_owner=current_owner, end_date="2024-01-01 00:00:00" - ) - - def test_plan_service_returns_plan_data_for_non_trial_basic_plan(self): - trial_start_date = None - trial_end_date = None - current_org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - ) - plan_service = PlanService(current_org=current_org) - - basic_plan = FREE_PLAN_REPRESENTATIONS[PlanName.BASIC_PLAN_NAME.value] - assert plan_service.current_org == current_org - assert plan_service.trial_status == TrialStatus.NOT_STARTED.value - assert plan_service.marketing_name == basic_plan.marketing_name - assert plan_service.plan_name == basic_plan.value - assert plan_service.tier_name == basic_plan.tier_name - assert plan_service.billing_rate == basic_plan.billing_rate - assert plan_service.base_unit_price == basic_plan.base_unit_price - assert plan_service.benefits == basic_plan.benefits - assert ( - plan_service.monthly_uploads_limit == basic_plan.monthly_uploads_limit - ) # should be 250 - assert ( - plan_service.monthly_uploads_limit == 250 - ) # should be 250 since not trialing - assert plan_service.trial_total_days == basic_plan.trial_days - - def test_plan_service_returns_plan_data_for_trialing_user_trial_plan(self): - trial_start_date = datetime.now() - trial_end_date = datetime.now() + timedelta( - days=TrialDaysAmount.CODECOV_SENTRY.value - ) - current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, - trial_start_date=trial_start_date, - trial_end_date=trial_end_date, - trial_status=TrialStatus.ONGOING.value, - ) - plan_service = PlanService(current_org=current_org) - - trial_plan = TRIAL_PLAN_REPRESENTATION[PlanName.TRIAL_PLAN_NAME.value] - assert plan_service.trial_status == TrialStatus.ONGOING.value - assert plan_service.marketing_name == trial_plan.marketing_name - assert plan_service.plan_name == trial_plan.value - assert plan_service.tier_name == trial_plan.tier_name - assert plan_service.billing_rate == trial_plan.billing_rate - assert plan_service.base_unit_price == trial_plan.base_unit_price - assert plan_service.benefits == trial_plan.benefits - assert plan_service.monthly_uploads_limit is None # Not 250 since it's trialing - assert plan_service.trial_total_days == trial_plan.trial_days - - def test_plan_service_sets_default_plan_data_values_correctly(self): - current_org = OwnerFactory( - plan=PlanName.CODECOV_PRO_MONTHLY.value, - stripe_subscription_id="test-sub-123", - plan_user_count=20, - plan_activated_users=[44], - plan_auto_activate=False, - ) - current_org.save() - - plan_service = PlanService(current_org=current_org) - plan_service.set_default_plan_data() - - assert current_org.plan == PlanName.BASIC_PLAN_NAME.value - assert current_org.plan_user_count == 1 - assert current_org.plan_activated_users is None - assert current_org.stripe_subscription_id is None - - def test_plan_service_returns_if_owner_has_trial_dates(self): - current_org = OwnerFactory( - plan=PlanName.CODECOV_PRO_MONTHLY.value, - trial_start_date=datetime.now(), - trial_end_date=datetime.now() + timedelta(days=14), - ) - current_org.save() - - plan_service = PlanService(current_org=current_org) - - assert plan_service.has_trial_dates == True - - def test_plan_service_has_seats_left(self): - current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, - plan_user_count=6, - plan_activated_users=[i for i in range(5)], - ) - plan_service = PlanService(current_org=current_org) - - assert plan_service.has_seats_left == True - - def test_plan_service_has_no_seats_left(self): - current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, - plan_user_count=5, - plan_activated_users=[i for i in range(5)], - ) - plan_service = PlanService(current_org=current_org) - - assert plan_service.has_seats_left == False - - def test_plan_service_update_plan_invalid_name(self): - current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) - plan_service = PlanService(current_org=current_org) - - with raises(ValueError, match="Unsupported plan"): - plan_service.update_plan(name="blah", user_count=1) - - def test_plan_service_update_plan_invalid_user_count(self): - current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) - plan_service = PlanService(current_org=current_org) - - with raises(ValueError, match="Quantity Needed"): - plan_service.update_plan( - name=PlanName.BASIC_PLAN_NAME.value, user_count=None - ) - - def test_plan_service_update_plan_succeeds(self): - current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) - plan_service = PlanService(current_org=current_org) - - plan_service.update_plan(name=PlanName.TEAM_MONTHLY.value, user_count=8) - - assert current_org.plan == PlanName.TEAM_MONTHLY.value - assert current_org.plan_user_count == 8 - - def test_has_account(self): - current_org = OwnerFactory() - plan_service = PlanService(current_org=current_org) - self.assertFalse(plan_service.has_account) - - current_org.account = AccountFactory() - current_org.save() - plan_service = PlanService(current_org=current_org) - self.assertTrue(plan_service.has_account) - - def test_plan_data_has_account(self): - current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) - plan_service = PlanService(current_org=current_org) - self.assertEqual(plan_service.plan_name, PlanName.BASIC_PLAN_NAME.value) - - current_org.account = AccountFactory(plan=PlanName.CODECOV_PRO_YEARLY.value) - current_org.save() - plan_service = PlanService(current_org=current_org) - self.assertEqual(plan_service.plan_name, PlanName.CODECOV_PRO_YEARLY.value) - - def test_plan_user_count_has_account(self): - org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value, plan_user_count=5) - account = AccountFactory( - plan=PlanName.BASIC_PLAN_NAME.value, plan_seat_count=50, free_seat_count=3 - ) - - plan_service = PlanService(current_org=org) - self.assertEqual(plan_service.plan_user_count, 5) - - org.account = account - org.save() - plan_service = PlanService(current_org=org) - self.assertEqual(plan_service.plan_user_count, 53) - - def test_has_seats_left_has_account(self): - org = OwnerFactory( - plan=PlanName.BASIC_PLAN_NAME.value, - plan_user_count=5, - plan_activated_users=[1, 2, 3], - ) - account = AccountFactory( - plan=PlanName.BASIC_PLAN_NAME.value, plan_seat_count=5, free_seat_count=3 - ) - for i in range(8): - AccountsUsersFactory(account=account) - - plan_service = PlanService(current_org=org) - self.assertEqual(plan_service.has_seats_left, True) - - org.account = account - org.save() - plan_service = PlanService(current_org=org) - self.assertEqual(plan_service.has_seats_left, False) - - -class AvailablePlansBeforeTrial(TestCase): - """ - - users-basic, no trial -> users-pr-inappm/y, users-basic - - users-free, no trial -> users-pr-inappm/y, users-basic, users-free - - users-teamm/y, no trial -> users-pr-inappm/y, users-basic, users-teamm/y - - users-pr-inappm/y, no trial -> users-pr-inappm/y, users-basic - - sentry customer, users-basic, no trial -> users-pr-inappm/y, users-sentrym/y, users-basic - - sentry customer, users-teamm/y, no trial -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y - - sentry customer, users-sentrym/y, no trial -> users-pr-inappm/y, users-sentrym/y, users-basic - """ - - def setUp(self): - self.current_org = OwnerFactory( - trial_start_date=None, - trial_end_date=None, - trial_status=TrialStatus.NOT_STARTED.value, - ) - self.owner = OwnerFactory() - - def test_available_plans_for_basic_plan_non_trial( - self, - ): - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - def test_available_plans_for_free_plan_non_trial( - self, - ): - self.current_org.plan = PlanName.FREE_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result.append(FREE_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - def test_available_plans_for_team_plan_non_trial( - self, - ): - self.current_org.plan = PlanName.TEAM_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - def test_available_plans_for_pro_plan_non_trial(self): - self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_customer_basic_plan_non_trial( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_customer_team_plan_non_trial( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.TEAM_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_plan_non_trial(self, is_sentry_user): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.SENTRY_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - -@freeze_time("2023-06-19") -class AvailablePlansExpiredTrialLessThanTenUsers(TestCase): - """ - - users-basic, has trialed, less than 10 users -> users-pr-inappm/y, users-basic, users-teamm/y - - users-teamm/y, has trialed, less than 10 users -> users-pr-inappm/y, users-basic, users-teamm/y - - users-pr-inappm/y, has trialed, less than 10 users -> users-pr-inappm/y, users-basic, users-teamm/y - - sentry customer, users-basic, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y - - sentry customer, users-teamm/y, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y - - sentry customer, users-sentrym/y, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y - """ - - def setUp(self): - self.current_org = OwnerFactory( - trial_start_date=datetime.now() + timedelta(days=-10), - trial_end_date=datetime.now() + timedelta(days=-3), - trial_status=TrialStatus.EXPIRED.value, - plan_user_count=3, - ) - self.owner = OwnerFactory() - - def test_available_plans_for_basic_plan_expired_trial_less_than_10_users( - self, - ): - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - def test_available_plans_for_team_plan_expired_trial_less_than_10_users( - self, - ): - self.current_org.plan = PlanName.TEAM_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - def test_available_plans_for_pro_plan_expired_trial_less_than_10_users(self): - self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_customer_basic_plan_expired_trial_less_than_10_users( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_customer_team_plan_expired_trial_less_than_10_users( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.TEAM_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_plan_expired_trial_less_than_10_users( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.SENTRY_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - -@freeze_time("2023-06-19") -class AvailablePlansExpiredTrialMoreThanTenActivatedUsers(TestCase): - """ - - users-pr-inappm/y, has trialed, more than 10 activated users -> users-pr-inappm/y, users-basic - - sentry customer, users-basic, has trialed, more than 10 activated users -> users-pr-inappm/y, users-sentrym/y, users-basic - - sentry customer, users-sentrym/y, has trialed, more than 10 activated users -> users-pr-inappm/y, users-sentrym/y, users-basic - """ - - def setUp(self): - self.current_org = OwnerFactory( - trial_start_date=datetime.now() + timedelta(days=-10), - trial_end_date=datetime.now() + timedelta(days=-3), - trial_status=TrialStatus.EXPIRED.value, - plan_user_count=1, - plan_activated_users=[i for i in range(13)], - ) - self.owner = OwnerFactory() - - def test_available_plans_for_pro_plan_expired_trial_more_than_10_users(self): - self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_customer_basic_plan_expired_trial_more_than_10_users( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.BASIC_PLAN_NAME.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_available_plans_for_sentry_plan_expired_trial_more_than_10_users( - self, is_sentry_user - ): - is_sentry_user.return_value = True - self.current_org.plan = PlanName.SENTRY_MONTHLY.value - self.current_org.save() - - plan_service = PlanService(current_org=self.current_org) - - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - - assert plan_service.available_plans(owner=self.owner) == expected_result - - -@freeze_time("2023-06-19") -class AvailablePlansExpiredTrialMoreThanTenSeatsLessThanTenActivatedUsers(TestCase): - """ - Tests that what matters for Team plan is activated users not the total seat count - """ - - def setUp(self): - self.expected_result = [] - self.expected_result.append(BASIC_PLAN) - self.expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - self.expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - def test_currently_team_plan(self): - self.current_org = OwnerFactory( - plan_user_count=100, - plan_activated_users=[i for i in range(10)], - plan=PlanName.TEAM_MONTHLY.value, - ) - self.owner = OwnerFactory() - self.plan_service = PlanService(current_org=self.current_org) - - assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result - ) - - def test_trial_expired(self): - self.current_org = OwnerFactory( - plan_user_count=100, - plan_activated_users=[i for i in range(10)], - trial_status=TrialStatus.EXPIRED.value, - trial_start_date=datetime.now() + timedelta(days=-10), - trial_end_date=datetime.now() + timedelta(days=-3), - ) - self.owner = OwnerFactory() - self.plan_service = PlanService(current_org=self.current_org) - - assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result - ) - - def test_trial_ongoing(self): - self.current_org = OwnerFactory( - plan_user_count=100, - plan_activated_users=[i for i in range(10)], - trial_status=TrialStatus.ONGOING.value, - trial_start_date=datetime.now() + timedelta(days=-10), - trial_end_date=datetime.now() + timedelta(days=3), - ) - self.owner = OwnerFactory() - self.plan_service = PlanService(current_org=self.current_org) - - assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result - ) - - def test_trial_not_started(self): - self.current_org = OwnerFactory( - plan_user_count=100, - plan_activated_users=[i for i in range(10)], - trial_status=TrialStatus.NOT_STARTED.value, - ) - self.owner = OwnerFactory() - self.plan_service = PlanService(current_org=self.current_org) - - self.expected_result = [] - self.expected_result.append(BASIC_PLAN) - self.expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - self.expected_result += TEAM_PLAN_REPRESENTATIONS.values() - assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result - ) - - -@freeze_time("2023-06-19") -class AvailablePlansOngoingTrial(TestCase): - """ - Non Sentry User is trialing - when <=10 activated seats -> users-pr-inappm/y, users-basic, users-teamm/y - when > 10 activated seats -> users-pr-inappm/y, users-basic - Sentry User is trialing - when <=10 activated seats -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y - when > 10 activated seats -> users-pr-inappm/y, users-sentrym/y, users-basic - """ - - def setUp(self): - self.current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, - trial_start_date=datetime.now(), - trial_end_date=datetime.now() + timedelta(days=14), - trial_status=TrialStatus.ONGOING.value, - plan_user_count=1000, - plan_activated_users=None, - ) - self.owner = OwnerFactory() - self.plan_service = PlanService(current_org=self.current_org) - - def test_non_sentry_user(self): - # [Basic, Pro Monthly, Pro Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - # Can do Team plan when plan_activated_users is null - assert self.plan_service.available_plans(owner=self.owner) == expected_result - - self.current_org.plan_activated_users = [i for i in range(10)] - self.current_org.save() - - # Can do Team plan when at 10 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result - - self.current_org.plan_activated_users = [i for i in range(11)] - self.current_org.save() - - # [Basic, Pro Monthly, Pro Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - - # Can not do Team plan when at 11 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result - - @patch("services.sentry.is_sentry_user") - def test_sentry_user(self, is_sentry_user): - is_sentry_user.return_value = True - - # [Basic, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - - # Can do Team plan when plan_activated_users is null - assert self.plan_service.available_plans(owner=self.owner) == expected_result - - self.current_org.plan_activated_users = [i for i in range(10)] - self.current_org.save() - - # Can do Team plan when at 10 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result - - self.current_org.plan_activated_users = [i for i in range(11)] - self.current_org.save() - - # [Basic, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - - # Can not do Team plan when at 11 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result diff --git a/services/billing.py b/services/billing.py index bfe6fc65ce..7ca12371a6 100644 --- a/services/billing.py +++ b/services/billing.py @@ -13,10 +13,10 @@ USER_PLAN_REPRESENTATIONS, PlanBillingRate, ) +from shared.plan.service import PlanService from billing.constants import REMOVED_INVOICE_STATUSES from codecov_auth.models import Owner -from plan.service import PlanService log = logging.getLogger(__name__) diff --git a/services/tests/test_billing.py b/services/tests/test_billing.py index 75804f5b2f..66b3b1b383 100644 --- a/services/tests/test_billing.py +++ b/services/tests/test_billing.py @@ -1891,7 +1891,7 @@ def test_update_plan_to_users_basic_deletes_subscription_if_user_has_stripe_subs ) delete_subscription_mock.assert_called_once_with(owner) - @patch("plan.service.PlanService.set_default_plan_data") + @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") @@ -1913,7 +1913,7 @@ def test_update_plan_to_users_basic_sets_plan_if_no_subscription_id( modify_subscription_mock.assert_not_called() create_checkout_session_mock.assert_not_called() - @patch("plan.service.PlanService.set_default_plan_data") + @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") @@ -1934,7 +1934,7 @@ def test_update_plan_modifies_subscription_if_user_plan_and_subscription_exists( delete_subscription_mock.assert_not_called() create_checkout_session_mock.assert_not_called() - @patch("plan.service.PlanService.set_default_plan_data") + @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") @@ -1955,7 +1955,7 @@ def test_update_plan_creates_checkout_session_if_user_plan_and_no_subscription( delete_subscription_mock.assert_not_called() modify_subscription_mock.assert_not_called() - @patch("plan.service.PlanService.set_default_plan_data") + @patch("shared.plan.service.PlanService.set_default_plan_data") @patch("services.tests.test_billing.MockPaymentService.create_checkout_session") @patch("services.tests.test_billing.MockPaymentService.modify_subscription") @patch("services.tests.test_billing.MockPaymentService.delete_subscription") diff --git a/upload/helpers.py b/upload/helpers.py index 330a56f111..428ffc9510 100644 --- a/upload/helpers.py +++ b/upload/helpers.py @@ -14,6 +14,7 @@ from rest_framework.exceptions import NotFound, Throttled, ValidationError from shared.github import InvalidInstallationError from shared.plan.constants import USER_PLAN_REPRESENTATIONS +from shared.plan.service import PlanService from shared.reports.enums import UploadType from shared.torngit.exceptions import TorngitClientError, TorngitObjectNotFoundError from shared.upload.utils import query_monthly_coverage_measurements @@ -26,7 +27,6 @@ Owner, ) from core.models import Commit, Repository -from plan.service import PlanService from reports.models import CommitReport, ReportSession from services.analytics import AnalyticsService from services.redis_configuration import get_redis_connection diff --git a/upload/throttles.py b/upload/throttles.py index 2f148c64dd..949ca689a8 100644 --- a/upload/throttles.py +++ b/upload/throttles.py @@ -5,10 +5,10 @@ from django.db.models import Q from rest_framework.exceptions import ValidationError from rest_framework.throttling import BaseThrottle +from shared.plan.service import PlanService from shared.reports.enums import UploadType from shared.upload.utils import query_monthly_coverage_measurements -from plan.service import PlanService from reports.models import ReportSession from services.redis_configuration import get_redis_connection from upload.helpers import _determine_responsible_owner