diff --git a/api/internal/owner/serializers.py b/api/internal/owner/serializers.py index 26584562f2..355723dff1 100644 --- a/api/internal/owner/serializers.py +++ b/api/internal/owner/serializers.py @@ -3,7 +3,6 @@ from typing import Any, Dict from dateutil.relativedelta import relativedelta -from django.conf import settings from rest_framework import serializers from rest_framework.exceptions import PermissionDenied from shared.plan.constants import ( @@ -217,11 +216,7 @@ class StripeScheduledPhaseSerializer(serializers.Serializer): def get_plan(self, phase: Dict[str, Any]) -> str: plan_id = phase["items"][0]["plan"] - stripe_plan_dict = settings.STRIPE_PLAN_IDS - plan_name = list(stripe_plan_dict.keys())[ - list(stripe_plan_dict.values()).index(plan_id) - ] - marketing_plan_name = Plan.objects.get(name=plan_name).marketing_name + marketing_plan_name = Plan.objects.get(stripe_id=plan_id).marketing_name return marketing_plan_name def get_quantity(self, phase: Dict[str, Any]) -> int: @@ -344,11 +339,13 @@ def update(self, instance: Owner, validated_data: Dict[str, Any]) -> object: instance, desired_plan ) - sentry_plans = Plan.objects.filter( - tier__tier_name=TierName.SENTRY.value, is_active=True - ).values_list("name", flat=True) + plan = ( + Plan.objects.select_related("tier") + .filter(name=desired_plan["value"]) + .first() + ) - if desired_plan["value"] in sentry_plans: + if plan and plan.tier.tier_name == TierName.SENTRY.value: current_owner = self.context["view"].request.current_owner send_sentry_webhook(current_owner, instance) diff --git a/api/internal/tests/views/test_account_viewset.py b/api/internal/tests/views/test_account_viewset.py index ed18ac6b64..0cb97d449a 100644 --- a/api/internal/tests/views/test_account_viewset.py +++ b/api/internal/tests/views/test_account_viewset.py @@ -234,7 +234,7 @@ def test_retrieve_account_gets_account_fields_when_there_are_scheduled_details( schedule_params = { "id": 123, "start_date": 123689126736, - "stripe_plan_id": "plan_H6P3KZXwmAbqPS", + "stripe_plan_id": "plan_pro_yearly", "quantity": 6, } phases = [ @@ -330,7 +330,7 @@ def test_retrieve_account_returns_last_phase_when_more_than_one_scheduled_phases schedule_params = { "id": 123, "start_date": 123689126736, - "stripe_plan_id": "plan_H6P3KZXwmAbqPS", + "stripe_plan_id": "plan_pro_yearly", "quantity": 6, } phases = [ diff --git a/billing/helpers.py b/billing/helpers.py index bd511c52e1..8bbef4df26 100644 --- a/billing/helpers.py +++ b/billing/helpers.py @@ -27,6 +27,8 @@ def get_all_admins_for_owners(owners: QuerySet[Owner]): def mock_all_plans_and_tiers(): + TierFactory(tier_name=TierName.BASIC.value) + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) PlanFactory( tier=trial_tier, @@ -39,18 +41,7 @@ def mock_all_plans_and_tiers(): "Unlimited private repositories", "Priority Support", ], - ) - - basic_tier = TierFactory(tier_name=TierName.BASIC.value) - PlanFactory( - name=PlanName.FREE_PLAN_NAME.value, - tier=basic_tier, - marketing_name="Developer", - benefits=[ - "Up to 1 user", - "Unlimited public repositories", - "Unlimited private repositories", - ], + stripe_id="plan_trial", ) pro_tier = TierFactory(tier_name=TierName.PRO.value) @@ -67,6 +58,7 @@ def mock_all_plans_and_tiers(): billing_rate=BillingRate.MONTHLY.value, base_unit_price=PlanPrice.MONTHLY.value, paid_plan=True, + stripe_id="plan_pro", ) PlanFactory( name=PlanName.CODECOV_PRO_YEARLY.value, @@ -81,6 +73,7 @@ def mock_all_plans_and_tiers(): billing_rate=BillingRate.ANNUALLY.value, base_unit_price=PlanPrice.YEARLY.value, paid_plan=True, + stripe_id="plan_pro_yearly", ) team_tier = TierFactory(tier_name=TierName.TEAM.value) @@ -98,6 +91,7 @@ def mock_all_plans_and_tiers(): base_unit_price=PlanPrice.TEAM_MONTHLY.value, monthly_uploads_limit=2500, paid_plan=True, + stripe_id="plan_team_monthly", ) PlanFactory( name=PlanName.TEAM_YEARLY.value, @@ -113,6 +107,7 @@ def mock_all_plans_and_tiers(): base_unit_price=PlanPrice.TEAM_YEARLY.value, monthly_uploads_limit=2500, paid_plan=True, + stripe_id="plan_team_yearly", ) sentry_tier = TierFactory(tier_name=TierName.SENTRY.value) @@ -130,6 +125,7 @@ def mock_all_plans_and_tiers(): "Unlimited private repositories", "Priority Support", ], + stripe_id="plan_sentry_monthly", ) PlanFactory( name=PlanName.SENTRY_YEARLY.value, @@ -145,6 +141,7 @@ def mock_all_plans_and_tiers(): "Unlimited private repositories", "Priority Support", ], + stripe_id="plan_sentry_yearly", ) enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) @@ -161,6 +158,7 @@ def mock_all_plans_and_tiers(): "Unlimited private repositories", "Priority Support", ], + stripe_id="plan_enterprise_cloud_monthly", ) PlanFactory( name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, @@ -175,6 +173,7 @@ def mock_all_plans_and_tiers(): "Unlimited private repositories", "Priority Support", ], + stripe_id="plan_enterprise_cloud_yearly", ) PlanFactory( @@ -190,4 +189,5 @@ def mock_all_plans_and_tiers(): "Unlimited public repositories", "Unlimited private repositories", ], + stripe_id="plan_default_free", ) diff --git a/billing/tests/test_views.py b/billing/tests/test_views.py index 69fc49cd8d..c2108f8a8c 100644 --- a/billing/tests/test_views.py +++ b/billing/tests/test_views.py @@ -13,6 +13,7 @@ from billing.helpers import mock_all_plans_and_tiers from billing.views import StripeWebhookHandler +from codecov_auth.models import Plan from ..constants import StripeHTTPHeaders @@ -721,7 +722,7 @@ def test_customer_subscription_created_early_returns_if_unverified_payment( "object": { "id": "sub_123", "customer": "cus_123", - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, } @@ -776,7 +777,7 @@ def test_customer_subscription_created_does_nothing_if_plan_not_paid_user_plan( "object": { "id": "FOEKDCDEQ", "customer": "sdo050493", - "plan": {"id": "?"}, + "plan": {"id": "plan_free"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, } @@ -809,7 +810,7 @@ def test_customer_subscription_created_sets_plan_info( "object": { "id": stripe_subscription_id, "customer": stripe_customer_id, - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": quantity, "status": "active", @@ -849,7 +850,7 @@ def test_customer_subscription_created_can_trigger_trial_expiration( "object": { "id": stripe_subscription_id, "customer": stripe_customer_id, - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": quantity, "default_payment_method": "blabla", @@ -882,7 +883,7 @@ def test_customer_subscription_updated_does_not_change_subscription_if_not_paid_ "object": { "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, - "plan": {"id": "?"}, + "plan": {"id": "plan_free"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, "status": "active", @@ -929,7 +930,7 @@ def test_customer_subscription_updated_does_not_change_subscription_if_there_is_ "object": { "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, "status": "active", @@ -985,7 +986,7 @@ def test_customer_subscription_updated_sets_free_and_deactivates_all_repos_if_in "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, "plan": { - "id": "plan_H6P16wij3lUuxg", + "id": "plan_pro_yearly", }, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, @@ -1088,7 +1089,7 @@ def test_customer_subscription_updated_sets_free_and_deactivates_all_repos_if_in "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, "plan": { - "id": "plan_H6P16wij3lUuxg", + "id": "plan_pro_yearly", }, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": 20, @@ -1149,7 +1150,7 @@ def test_customer_subscription_updated_sets_fields_on_success( "object": { "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": quantity, "status": "active", @@ -1200,7 +1201,7 @@ def test_customer_subscription_updated_sets_fields_on_success_multiple_owner( "object": { "id": self.owner.stripe_subscription_id, "customer": self.owner.stripe_customer_id, - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": self.owner.ownerid}, "quantity": quantity, "status": "active", @@ -1238,7 +1239,7 @@ def test_customer_subscription_updated_logs_error_if_no_matching_owners( "object": { "id": "sub_notexist", "customer": "cus_notexist", - "plan": {"id": "plan_H6P16wij3lUuxg"}, + "plan": {"id": "plan_pro_yearly"}, "metadata": {"obo_organization": 1}, "quantity": 8, "status": "active", @@ -1254,7 +1255,7 @@ def test_customer_subscription_updated_logs_error_if_no_matching_owners( extra={ "stripe_subscription_id": "sub_notexist", "stripe_customer_id": "cus_notexist", - "plan_id": "plan_H6P16wij3lUuxg", + "plan_id": "plan_pro_yearly", }, ) @@ -1267,7 +1268,7 @@ def test_subscription_schedule_released_updates_owner_with_existing_subscription self.owner.save() self.new_params = { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", } @@ -1288,7 +1289,8 @@ def test_subscription_schedule_released_updates_owner_with_existing_subscription ) self.owner.refresh_from_db() - assert self.owner.plan == settings.STRIPE_PLAN_VALS[self.new_params["new_plan"]] + plan = Plan.objects.get(stripe_id=self.new_params["new_plan"]) + assert self.owner.plan == plan.name assert self.owner.plan_user_count == self.new_params["new_quantity"] @patch("services.billing.stripe.Subscription.retrieve") @@ -1304,7 +1306,7 @@ def test_subscription_schedule_released_updates_multiple_owners_with_existing_su self.other_owner.save() self.new_params = { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", } @@ -1326,12 +1328,11 @@ def test_subscription_schedule_released_updates_multiple_owners_with_existing_su self.owner.refresh_from_db() self.other_owner.refresh_from_db() - assert self.owner.plan == settings.STRIPE_PLAN_VALS[self.new_params["new_plan"]] + + plan = Plan.objects.get(stripe_id=self.new_params["new_plan"]) + assert self.owner.plan == plan.name assert self.owner.plan_user_count == self.new_params["new_quantity"] - assert ( - self.other_owner.plan - == settings.STRIPE_PLAN_VALS[self.new_params["new_plan"]] - ) + assert self.other_owner.plan == plan.name assert self.other_owner.plan_user_count == self.new_params["new_quantity"] @patch("logging.Logger.error") @@ -1342,7 +1343,7 @@ def test_subscription_schedule_released_logs_error_if_owner_does_not_exist( log_error_mock, ): self.new_params = { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_notexist", } @@ -1367,7 +1368,7 @@ def test_subscription_schedule_released_logs_error_if_owner_does_not_exist( extra={ "stripe_subscription_id": "sub_notexist", "stripe_customer_id": "cus_123", - "plan_id": "plan_H6P3KZXwmAbqPS", + "plan_id": "plan_pro_yearly", }, ) @@ -1383,7 +1384,7 @@ def test_subscription_schedule_created_logs_a_new_schedule( self.owner.save() self.params = { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": subscription_id, } @@ -1410,7 +1411,7 @@ def test_subscription_schedule_updated_logs_changes_to_schedule( original_plan = "users-pr-inappy" original_quantity = 10 subscription_id = "sub_1K8xfkGlVGuVgOrkxvroyZdH" - new_plan = "plan_H6P3KZXwmAbqPS" + new_plan = "plan_pro_yearly" new_quantity = 7 self.owner.plan = original_plan self.owner.plan_user_count = original_quantity diff --git a/billing/views.py b/billing/views.py index 974601c3fc..58906f6ea2 100644 --- a/billing/views.py +++ b/billing/views.py @@ -13,7 +13,7 @@ from shared.plan.service import PlanService from billing.helpers import get_all_admins_for_owners -from codecov_auth.models import Owner +from codecov_auth.models import Owner, Plan from services.task.task import TaskService from .constants import StripeHTTPHeaders, StripeWebhookEvents @@ -203,7 +203,7 @@ def subscription_schedule_created( ) -> None: subscription = stripe.Subscription.retrieve(schedule["subscription"]) sub_item_plan_id = subscription.plan.id - plan_name = settings.STRIPE_PLAN_VALS[sub_item_plan_id] + plan_name = Plan.objects.get(stripe_id=sub_item_plan_id).name log.info( "Schedule created for customer", extra=dict( @@ -223,10 +223,7 @@ def subscription_schedule_updated( scheduled_phase = schedule["phases"][-1] scheduled_plan = scheduled_phase["items"][0] plan_id = scheduled_plan["plan"] - stripe_plan_dict = settings.STRIPE_PLAN_IDS - plan_name = list(stripe_plan_dict.keys())[ - list(stripe_plan_dict.values()).index(plan_id) - ] + plan_name = Plan.objects.get(stripe_id=plan_id).name quantity = scheduled_plan["quantity"] log.info( "Schedule updated for customer", @@ -259,7 +256,7 @@ def subscription_schedule_released( return sub_item_plan_id = subscription.plan.id - plan_name = settings.STRIPE_PLAN_VALS[sub_item_plan_id] + plan_name = Plan.objects.get(stripe_id=sub_item_plan_id).name for owner in owners: plan_service = PlanService(current_org=owner) plan_service.update_plan(name=plan_name, user_count=subscription.quantity) @@ -302,7 +299,9 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No ) return - if sub_item_plan_id not in settings.STRIPE_PLAN_VALS: + try: + plan = Plan.objects.get(stripe_id=sub_item_plan_id) + except Plan.DoesNotExist: log.warning( "Subscription creation requested for invalid plan", extra=dict( @@ -313,7 +312,7 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No ) return - plan_name = settings.STRIPE_PLAN_VALS[sub_item_plan_id] + plan_name = plan.name log.info( "Subscription created for customer", @@ -441,7 +440,9 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No invoice_settings={"default_payment_method": default_payment_method}, ) - if subscription.plan.id not in settings.STRIPE_PLAN_VALS: + try: + plan = Plan.objects.get(stripe_id=subscription.plan.id) + except Plan.DoesNotExist: log.error( "Subscription update requested with invalid plan", extra=dict( @@ -453,7 +454,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No return subscription_schedule_id = subscription.schedule - plan_name = settings.STRIPE_PLAN_VALS[subscription.plan.id] + plan_name = plan.name incomplete_expired = subscription.status == "incomplete_expired" # Only update if there is not a scheduled subscription diff --git a/codecov/settings_dev.py b/codecov/settings_dev.py index 7b643922e5..dc882f4ef9 100644 --- a/codecov/settings_dev.py +++ b/codecov/settings_dev.py @@ -18,23 +18,6 @@ STRIPE_ENDPOINT_SECRET = get_config( "services", "stripe", "endpoint_secret", default="default" ) -STRIPE_PLAN_IDS = { - "users-pr-inappm": "plan_H6P3KZXwmAbqPS", - "users-pr-inappy": "plan_H6P16wij3lUuxg", - "users-sentrym": "price_1Mj1kYGlVGuVgOrk7jucaZAa", - "users-sentryy": "price_1Mj1mMGlVGuVgOrkC0ORc6iW", - "users-teamm": "price_1OCM0gGlVGuVgOrkWDYEBtSL", - "users-teamy": "price_1OCM2cGlVGuVgOrkMWUFjPFz", -} - -STRIPE_PLAN_VALS = { - "plan_H6P3KZXwmAbqPS": "users-pr-inappm", - "plan_H6P16wij3lUuxg": "users-pr-inappy", - "price_1Mj1kYGlVGuVgOrk7jucaZAa": "users-sentrym", - "price_1Mj1mMGlVGuVgOrkC0ORc6iW": "users-sentryy", - "price_1OCM0gGlVGuVgOrkWDYEBtSL": "users-teamm", - "price_1OCM2cGlVGuVgOrkMWUFjPFz": "users-teamy", -} CORS_ALLOW_CREDENTIALS = True diff --git a/codecov/settings_prod.py b/codecov/settings_prod.py index 978656c84e..ca3c2c91d6 100644 --- a/codecov/settings_prod.py +++ b/codecov/settings_prod.py @@ -15,27 +15,6 @@ STRIPE_API_KEY = os.environ.get("SERVICES__STRIPE__API_KEY", None) STRIPE_ENDPOINT_SECRET = os.environ.get("SERVICES__STRIPE__ENDPOINT_SECRET", None) -STRIPE_PLAN_IDS = { - "users-pr-inappm": "price_1Gv2B8GlVGuVgOrkFnLunCgc", - "users-pr-inappy": "price_1Gv2COGlVGuVgOrkuOYVLIj7", - "users-sentrym": "price_1MlY9yGlVGuVgOrkHluurBtJ", - "users-sentryy": "price_1MlYAYGlVGuVgOrke9SdbBUn", - "users-enterprisey": "price_1LmjzwGlVGuVgOrkIwlM46EU", - "users-enterprisem": "price_1LmjypGlVGuVgOrkzKtNqhwW", - "users-teamm": "price_1NqPKdGlVGuVgOrkm9OFvtz8", - "users-teamy": "price_1NrlXiGlVGuVgOrkgMTw5yno", -} - -STRIPE_PLAN_VALS = { - "price_1Gv2B8GlVGuVgOrkFnLunCgc": "users-pr-inappm", - "price_1Gv2COGlVGuVgOrkuOYVLIj7": "users-pr-inappy", - "price_1MlY9yGlVGuVgOrkHluurBtJ": "users-sentrym", - "price_1MlYAYGlVGuVgOrke9SdbBUn": "users-sentryy", - "price_1LmjzwGlVGuVgOrkIwlM46EU": "users-enterprisey", - "price_1LmjypGlVGuVgOrkzKtNqhwW": "users-enterprisem", - "price_1NqPKdGlVGuVgOrkm9OFvtz8": "users-teamm", - "price_1NrlXiGlVGuVgOrkgMTw5yno": "users-teamy", -} CORS_ALLOW_HEADERS += ["sentry-trace", "baggage"] CORS_ALLOW_CREDENTIALS = True diff --git a/codecov/settings_staging.py b/codecov/settings_staging.py index 2872e630e7..1049e192de 100644 --- a/codecov/settings_staging.py +++ b/codecov/settings_staging.py @@ -18,22 +18,6 @@ STRIPE_ENDPOINT_SECRET = os.environ.get("SERVICES__STRIPE__ENDPOINT_SECRET", None) COOKIES_DOMAIN = ".codecov.dev" SESSION_COOKIE_DOMAIN = ".codecov.dev" -STRIPE_PLAN_IDS = { - "users-pr-inappm": "plan_H6P3KZXwmAbqPS", - "users-pr-inappy": "plan_H6P16wij3lUuxg", - "users-sentrym": "price_1Mj1kYGlVGuVgOrk7jucaZAa", - "users-sentryy": "price_1Mj1mMGlVGuVgOrkC0ORc6iW", - "users-teamm": "price_1OCM0gGlVGuVgOrkWDYEBtSL", - "users-teamy": "price_1OCM2cGlVGuVgOrkMWUFjPFz", -} -STRIPE_PLAN_VALS = { - "plan_H6P3KZXwmAbqPS": "users-pr-inappm", - "plan_H6P16wij3lUuxg": "users-pr-inappy", - "price_1Mj1kYGlVGuVgOrk7jucaZAa": "users-sentrym", - "price_1Mj1mMGlVGuVgOrkC0ORc6iW": "users-sentryy", - "price_1OCM0gGlVGuVgOrkWDYEBtSL": "users-teamm", - "price_1OCM2cGlVGuVgOrkMWUFjPFz": "users-teamy", -} CORS_ALLOW_HEADERS += ["sentry-trace", "baggage"] CORS_ALLOWED_ORIGIN_REGEXES = [ diff --git a/codecov_auth/management/commands/set_trial_status_values.py b/codecov_auth/management/commands/set_trial_status_values.py deleted file mode 100644 index 7872bbce57..0000000000 --- a/codecov_auth/management/commands/set_trial_status_values.py +++ /dev/null @@ -1,114 +0,0 @@ -from datetime import datetime -from typing import Any - -from django.core.management.base import BaseCommand, CommandParser -from django.db.models import Q -from shared.plan.constants import ( - FREE_PLAN_REPRESENTATIONS, - PLANS_THAT_CAN_TRIAL, - PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS, - SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - PlanName, - TrialStatus, -) - -from codecov_auth.models import Owner - - -class Command(BaseCommand): - help = "Sets the initial trial status values for an owner" - - def add_arguments(self, parser: CommandParser) -> None: - parser.add_argument("trial_status_type", type=str) - - def handle(self, *args: Any, **options: Any) -> None: - trial_status_type = options.get("trial_status_type", {}) - - # NOT_STARTED - if trial_status_type == "all" or trial_status_type == "not_started": - # Free plan customers - Owner.objects.filter( - plan__in=FREE_PLAN_REPRESENTATIONS, - stripe_customer_id=None, - ).update(trial_status=TrialStatus.NOT_STARTED.value) - - # ONGOING - if trial_status_type == "all" or trial_status_type == "ongoing": - Owner.objects.filter( - plan__in=SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - trial_end_date__gt=datetime.now(), - ).update(trial_status=TrialStatus.ONGOING.value) - - # EXPIRED - if trial_status_type == "all" or trial_status_type == "expired": - Owner.objects.filter( - # Currently paying sentry customer with trial_end_date - Q( - plan__in=SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - stripe_customer_id__isnull=False, - stripe_subscription_id__isnull=False, - trial_end_date__lte=datetime.now(), - ) - # Currently paying sentry customer without trial_end_date - | Q( - plan__in=SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - stripe_customer_id__isnull=False, - stripe_subscription_id__isnull=False, - trial_start_date__isnull=True, - trial_end_date__isnull=True, - ) - # Previously paid but now back to basic with trial start/end dates - | Q( - plan=PlanName.BASIC_PLAN_NAME.value, - stripe_customer_id__isnull=False, - trial_start_date__isnull=False, - trial_end_date__isnull=False, - ) - ).update(trial_status=TrialStatus.EXPIRED.value) - - # CANNOT_TRIAL - if trial_status_type == "all" or trial_status_type == "cannot_trial": - Owner.objects.filter( - # Plans that cannot trial - ~Q(plan__in=PLANS_THAT_CAN_TRIAL) - # Previously paid but now back to basic without trial start/end dates - | Q( - plan=PlanName.BASIC_PLAN_NAME.value, - stripe_customer_id__isnull=False, - trial_start_date__isnull=True, - trial_end_date__isnull=True, - ) - # Currently paying customer that isn't a sentry plan (they would be expired) - | Q( - ~Q(plan__in=SENTRY_PAID_USER_PLAN_REPRESENTATIONS), - stripe_subscription_id__isnull=False, - stripe_customer_id__isnull=False, - ) - # Invoiced customers without stripe info - | Q( - Q(plan__in=PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS), - stripe_subscription_id__isnull=True, - stripe_customer_id__isnull=True, - ) - ).update(trial_status=TrialStatus.CANNOT_TRIAL.value) - - # DELETE ALL - in case something gets messed up - if trial_status_type == "null_all_are_you_sure": - Owner.objects.all().update(trial_status=None) - - -# Scenarios -# basic plan, without billing_info > not_started - -# sentry plan, billing_info, with end date after today > ongoing - -# sentry plan, billing_info, with end date before today > expired -# sentry plan, billing_info, subscription id > expired -# basic plan, with billing_info, with start/end dates > expired - -# unsupported trial plan > cannot_trial -# supported paid plan, no end date > cannot_trial -# basic plan, with billing_info, no start/end dates > cannot_trial -# invoiced customers, pro plans, no billing_info > cannot_trial - -# supported paid plan, with end date > should currently not exist in the DB diff --git a/codecov_auth/management/commands/tests/test_set_trial_status_values.py b/codecov_auth/management/commands/tests/test_set_trial_status_values.py deleted file mode 100644 index d3fdc3c219..0000000000 --- a/codecov_auth/management/commands/tests/test_set_trial_status_values.py +++ /dev/null @@ -1,168 +0,0 @@ -from datetime import datetime, timedelta - -from django.core.management.base import BaseCommand -from django.test import TestCase -from freezegun import freeze_time -from shared.django_apps.core.tests.factories import OwnerFactory -from shared.plan.constants import PlanName, TrialStatus - -from codecov_auth.management.commands.set_trial_status_values import Command -from codecov_auth.models import Owner - - -@freeze_time("2023-07-17T00:00:00") -class OwnerCommandTestCase(TestCase): - def setUp(self): - self.command_instance = BaseCommand() - now = datetime.now() - later = now + timedelta(days=3) - yesterday = now + timedelta(days=-1) - much_before = now + timedelta(days=-20) - - # NOT_STARTED - self.not_started_basic_owner = OwnerFactory( - username="one", - service="github", - plan=PlanName.BASIC_PLAN_NAME.value, - stripe_customer_id=None, - trial_status=None, - ) - self.not_started_free_owner = OwnerFactory( - username="two", - service="github", - plan=PlanName.FREE_PLAN_NAME.value, - stripe_customer_id=None, - trial_status=None, - ) - - # ONGOING - self.sentry_plan_ongoing_owner = OwnerFactory( - username="three", - service="github", - plan=PlanName.SENTRY_MONTHLY.value, - trial_start_date=now, - trial_end_date=later, - trial_status=None, - ) - - # EXPIRED - self.sentry_plan_expired_owner_with_trial_dates = OwnerFactory( - username="four", - service="github", - plan=PlanName.SENTRY_MONTHLY.value, - stripe_customer_id="test-cus-123", - stripe_subscription_id="test-sub-123", - trial_start_date=much_before, - trial_end_date=yesterday, - trial_status=None, - ) - self.sentry_expired_owner_without_trial_dates = OwnerFactory( - username="five", - service="github", - plan=PlanName.SENTRY_YEARLY.value, - stripe_customer_id="test-cus-123", - stripe_subscription_id="test-sub-123", - trial_start_date=None, - trial_end_date=None, - trial_status=None, - ) - self.expired_owner_with_basic_plan_with_trial_dates = OwnerFactory( - username="six", - service="github", - plan=PlanName.BASIC_PLAN_NAME.value, - trial_start_date=much_before, - trial_end_date=yesterday, - stripe_customer_id="test-cus-123", - trial_status=None, - ) - - # CANNOT_TRIAL - self.unsupported_trial_plan_owner = OwnerFactory( - username="seven", - service="github", - plan=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, - trial_status=None, - ) - self.currently_paid_owner_without_trial_dates = OwnerFactory( - username="eight", - service="github", - plan=PlanName.CODECOV_PRO_MONTHLY.value, - stripe_customer_id="test-cus-123", - stripe_subscription_id="test-sub-123", - trial_start_date=None, - trial_end_date=None, - trial_status=None, - ) - self.previously_paid_owner_that_is_now_basic = OwnerFactory( - username="nine", - service="github", - plan=PlanName.BASIC_PLAN_NAME.value, - stripe_customer_id="test-cus-123", - trial_start_date=None, - trial_end_date=None, - trial_status=None, - ) - self.invoiced_customer_monthly_plan = OwnerFactory( - username="ten", - service="github", - plan=PlanName.CODECOV_PRO_MONTHLY.value, - stripe_customer_id=None, - stripe_subscription_id=None, - ) - self.invoiced_customer_yearly_plan = OwnerFactory( - username="eleven", - service="github", - plan=PlanName.CODECOV_PRO_YEARLY.value, - stripe_customer_id=None, - stripe_subscription_id=None, - ) - - def test_set_trial_status_values(self): - Command.handle(self.command_instance, trial_status_type="all") - - all_owners = Owner.objects.all() - - assert ( - all_owners.filter(username="one").first().trial_status - == TrialStatus.NOT_STARTED.value - ) - assert ( - all_owners.filter(username="two").first().trial_status - == TrialStatus.NOT_STARTED.value - ) - assert ( - all_owners.filter(username="three").first().trial_status - == TrialStatus.ONGOING.value - ) - assert ( - all_owners.filter(username="four").first().trial_status - == TrialStatus.EXPIRED.value - ) - assert ( - all_owners.filter(username="five").first().trial_status - == TrialStatus.EXPIRED.value - ) - assert ( - all_owners.filter(username="six").first().trial_status - == TrialStatus.EXPIRED.value - ) - assert ( - all_owners.filter(username="seven").first().trial_status - == TrialStatus.CANNOT_TRIAL.value - ) - assert ( - all_owners.filter(username="eight").first().trial_status - == TrialStatus.CANNOT_TRIAL.value - ) - assert ( - all_owners.filter(username="nine").first().trial_status - == TrialStatus.CANNOT_TRIAL.value - ) - assert ( - all_owners.filter(username="ten").first().trial_status - == TrialStatus.CANNOT_TRIAL.value - ) - assert ( - all_owners.filter(username="eleven").first().trial_status - == TrialStatus.CANNOT_TRIAL.value - ) diff --git a/codecov_auth/tests/test_admin.py b/codecov_auth/tests/test_admin.py index 7c5d0e0764..6b36c50027 100644 --- a/codecov_auth/tests/test_admin.py +++ b/codecov_auth/tests/test_admin.py @@ -28,7 +28,6 @@ from shared.django_apps.core.tests.factories import PullFactory, RepositoryFactory from shared.plan.constants import ( DEFAULT_FREE_PLAN, - ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS, PlanName, ) @@ -202,7 +201,6 @@ def test_inline_orgwide_add_token_permission_no_token_and_user_in_enterprise_clo self, ): owner = OwnerFactory(plan=DEFAULT_FREE_PLAN) - assert owner.plan not in ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0 request_url = reverse("admin:codecov_auth_owner_change", args=[owner.ownerid]) request = RequestFactory().get(request_url) diff --git a/services/billing.py b/services/billing.py index 184ba7acb7..d945b2e85d 100644 --- a/services/billing.py +++ b/services/billing.py @@ -316,9 +316,21 @@ def get_schedule(self, owner: Owner): return stripe.SubscriptionSchedule.retrieve(subscription_schedule_id) @_log_stripe_error - def modify_subscription(self, owner, desired_plan): + def modify_subscription(self, owner: Owner, desired_plan: dict): + desired_plan_info = Plan.objects.filter(name=desired_plan["value"]).first() + if not desired_plan_info: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + subscription = stripe.Subscription.retrieve(owner.stripe_subscription_id) - proration_behavior = self._get_proration_params(owner, desired_plan) + proration_behavior = self._get_proration_params( + owner, + desired_plan_info=desired_plan_info, + desired_quantity=desired_plan["quantity"], + ) subscription_schedule_id = subscription.schedule # proration_behavior indicates whether we immediately invoice a user or not. We only immediately @@ -326,7 +338,11 @@ def modify_subscription(self, owner, desired_plan): # An increase in seats and/or plan implies the user is upgrading, hence 'is_upgrading' is a consequence # of proration_behavior providing an invoice, in this case, != "none" # TODO: change this to "self._is_upgrading_seats(owner, desired_plan) or self._is_extending_term(owner, desired_plan)" - is_upgrading = True if proration_behavior != "none" else False + is_upgrading = ( + True + if proration_behavior != "none" and desired_plan_info.stripe_id + else False + ) # Divide logic bw immediate updates and scheduled updates # Immediate updates: when user upgrades seats or plan @@ -335,7 +351,6 @@ def modify_subscription(self, owner, desired_plan): # Scheduled updates: when the user decreases seats or plan # If the user is not in a schedule, create a schedule # If the user is in a schedule, update the existing schedule - if is_upgrading: if subscription_schedule_id: log.info( @@ -345,13 +360,14 @@ def modify_subscription(self, owner, desired_plan): log.info( f"Updating Stripe subscription for owner {owner.ownerid} to {desired_plan['value']} by user #{self.requesting_user.ownerid}" ) + subscription = stripe.Subscription.modify( owner.stripe_subscription_id, cancel_at_period_end=False, items=[ { "id": subscription["items"]["data"][0]["id"], - "plan": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "plan": desired_plan_info.stripe_id, "quantity": desired_plan["quantity"], } ], @@ -395,7 +411,11 @@ def modify_subscription(self, owner, desired_plan): ) def _modify_subscription_schedule( - self, owner: Owner, subscription, subscription_schedule_id, desired_plan + self, + owner: Owner, + subscription: stripe.Subscription, + subscription_schedule_id: str, + desired_plan: dict, ): current_subscription_start_date = subscription["current_period_start"] current_subscription_end_date = subscription["current_period_end"] @@ -404,6 +424,14 @@ def _modify_subscription_schedule( current_plan = subscription_item["plan"]["id"] current_quantity = subscription_item["quantity"] + plan = Plan.objects.filter(name=desired_plan["value"]).first() + if not plan or not plan.stripe_id: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + stripe.SubscriptionSchedule.modify( subscription_schedule_id, end_behavior="release", @@ -425,8 +453,8 @@ def _modify_subscription_schedule( "end_date": current_subscription_end_date + SCHEDULE_RELEASE_OFFSET, "items": [ { - "plan": settings.STRIPE_PLAN_IDS[desired_plan["value"]], - "price": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "plan": plan.stripe_id, + "price": plan.stripe_id, "quantity": desired_plan["quantity"], } ], @@ -436,20 +464,18 @@ def _modify_subscription_schedule( metadata=self._get_checkout_session_and_subscription_metadata(owner), ) - def _is_upgrading_seats(self, owner: Owner, desired_plan: dict) -> bool: + def _is_upgrading_seats(self, owner: Owner, desired_quantity: int) -> bool: """ Returns `True` if purchasing more seats. """ - return bool( - owner.plan_user_count and owner.plan_user_count < desired_plan["quantity"] - ) + return bool(owner.plan_user_count and owner.plan_user_count < desired_quantity) - def _is_extending_term(self, owner: Owner, desired_plan: dict) -> bool: + def _is_extending_term( + self, current_plan_info: Plan, desired_plan_info: Plan + ) -> bool: """ Returns `True` if switching from monthly to yearly plan. """ - current_plan_info = Plan.objects.get(name=owner.plan) - desired_plan_info = Plan.objects.get(name=desired_plan["value"]) return bool( current_plan_info @@ -458,15 +484,16 @@ def _is_extending_term(self, owner: Owner, desired_plan: dict) -> bool: and desired_plan_info.billing_rate == PlanBillingRate.YEARLY.value ) - def _is_similar_plan(self, owner: Owner, desired_plan: dict) -> bool: + def _is_similar_plan( + self, + owner: Owner, + current_plan_info: Plan, + desired_plan_info: Plan, + desired_quantity: int, + ) -> bool: """ Returns `True` if switching to a plan with similar term and seats. """ - current_plan_info = Plan.objects.select_related("tier").get(name=owner.plan) - desired_plan_info = Plan.objects.select_related("tier").get( - name=desired_plan["value"] - ) - is_same_term = ( current_plan_info and desired_plan_info @@ -474,7 +501,7 @@ def _is_similar_plan(self, owner: Owner, desired_plan: dict) -> bool: ) is_same_seats = ( - owner.plan_user_count and owner.plan_user_count == desired_plan["quantity"] + owner.plan_user_count and owner.plan_user_count == desired_quantity ) # If from PRO to TEAM, then not a similar plan if ( @@ -491,17 +518,27 @@ def _is_similar_plan(self, owner: Owner, desired_plan: dict) -> bool: return bool(is_same_term and is_same_seats) - def _get_proration_params(self, owner: Owner, desired_plan: dict) -> str: + def _get_proration_params( + self, owner: Owner, desired_plan_info: Plan, desired_quantity: int + ) -> str: + current_plan_info = Plan.objects.select_related("tier").get(name=owner.plan) if ( - self._is_upgrading_seats(owner, desired_plan) - or self._is_extending_term(owner, desired_plan) - or self._is_similar_plan(owner, desired_plan) + self._is_upgrading_seats(owner=owner, desired_quantity=desired_quantity) + or self._is_extending_term( + current_plan_info=current_plan_info, desired_plan_info=desired_plan_info + ) + or self._is_similar_plan( + owner=owner, + current_plan_info=current_plan_info, + desired_plan_info=desired_plan_info, + desired_quantity=desired_quantity, + ) ): return "always_invoice" else: return "none" - def _get_success_and_cancel_url(self, owner): + def _get_success_and_cancel_url(self, owner: Owner): short_services = {"github": "gh", "bitbucket": "bb", "gitlab": "gl"} base_path = f"/plan/{short_services[owner.service]}/{owner.username}" success_url = f"{settings.CODECOV_DASHBOARD_URL}{base_path}?success" @@ -509,13 +546,21 @@ def _get_success_and_cancel_url(self, owner): return success_url, cancel_url @_log_stripe_error - def create_checkout_session(self, owner: Owner, desired_plan): + def create_checkout_session(self, owner: Owner, desired_plan: dict): success_url, cancel_url = self._get_success_and_cancel_url(owner) log.info( "Creating Stripe Checkout Session for owner", extra=dict(owner_id=owner.ownerid), ) + plan = Plan.objects.filter(name=desired_plan["value"]).first() + if not plan or not plan.stripe_id: + log.error( + f"Plan {desired_plan['value']} not found", + extra=dict(owner_id=owner.ownerid), + ) + return + session = stripe.checkout.Session.create( payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, billing_address_collection="required", @@ -527,7 +572,7 @@ def create_checkout_session(self, owner: Owner, desired_plan): mode="subscription", line_items=[ { - "price": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "price": plan.stripe_id, "quantity": desired_plan["quantity"], } ], diff --git a/services/tests/test_billing.py b/services/tests/test_billing.py index 4e8d490e7f..be880a0fba 100644 --- a/services/tests/test_billing.py +++ b/services/tests/test_billing.py @@ -12,7 +12,7 @@ from stripe.api_resources import PaymentIntent, SetupIntent from billing.helpers import mock_all_plans_and_tiers -from codecov_auth.models import Service +from codecov_auth.models import Plan, Service from services.billing import AbstractPaymentService, BillingService, StripeService SCHEDULE_RELEASE_OFFSET = 10 @@ -195,13 +195,14 @@ def test_stripe_service_requires_requesting_user_to_be_owner_instance(self): def _assert_subscription_modify( self, subscription_modify_mock, owner, subscription_params, desired_plan ): + plan = Plan.objects.get(name=desired_plan["value"]) subscription_modify_mock.assert_called_once_with( owner.stripe_subscription_id, cancel_at_period_end=False, items=[ { "id": subscription_params["id"], - "plan": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "plan": plan.stripe_id, "quantity": desired_plan["quantity"], } ], @@ -225,6 +226,7 @@ def _assert_schedule_modify( desired_plan, schedule_id, ): + plan = Plan.objects.get(name=desired_plan["value"]) schedule_modify_mock.assert_called_once_with( schedule_id, end_behavior="release", @@ -247,8 +249,8 @@ def _assert_schedule_modify( + SCHEDULE_RELEASE_OFFSET, "items": [ { - "plan": settings.STRIPE_PLAN_IDS[desired_plan["value"]], - "price": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "plan": plan.stripe_id, + "price": plan.stripe_id, "quantity": desired_plan["quantity"], } ], @@ -437,7 +439,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri "name": plan, "id": 215, "plan": { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", "interval": "month", @@ -512,7 +514,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri "name": plan, "id": 215, "plan": { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", "interval": "year", @@ -589,7 +591,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri "name": plan, "id": 215, "plan": { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", "interval": "year", @@ -662,7 +664,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri "name": plan, "id": 215, "plan": { - "new_plan": "plan_H6P3KZXwmAbqPS", + "new_plan": "plan_pro_yearly", "new_quantity": 7, "subscription_id": "sub_123", "interval": "year", @@ -692,6 +694,36 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri assert owner.plan_activated_users == [4, 6, 3] assert owner.plan_user_count == 9 + @patch("logging.Logger.error") + def test_modify_subscription_no_plan_found( + self, + log_error_mock, + ): + original_plan = PlanName.CODECOV_PRO_MONTHLY.value + original_user_count = 10 + owner = OwnerFactory( + plan=original_plan, + plan_user_count=original_user_count, + stripe_subscription_id="33043sdf", + ) + + desired_plan_name = "invalid plan" + desired_user_count = 10 + desired_plan = {"value": desired_plan_name, "quantity": desired_user_count} + self.stripe.modify_subscription(owner, desired_plan) + + owner.refresh_from_db() + assert owner.plan == original_plan + assert owner.plan_user_count == original_user_count + log_error_mock.assert_has_calls( + [ + call( + f"Plan {desired_plan_name} not found", + extra=dict(owner_id=owner.ownerid), + ), + ] + ) + @patch("services.billing.stripe.Subscription.modify") @patch("services.billing.stripe.Subscription.retrieve") def test_modify_subscription_without_schedule_increases_user_count_immediately( @@ -1220,7 +1252,7 @@ def test_modify_subscription_with_schedule_modifies_schedule_when_plan_downgrade "start_date": current_subscription_start_date, "end_date": current_subscription_end_date, "quantity": original_user_count, - "name": original_plan, + "name": Plan.objects.get(name=original_plan).stripe_id, "id": 110, } @@ -1383,121 +1415,191 @@ def test_modify_subscription_with_schedule_releases_schedule_when_plan_downgrade def test_get_proration_params(self): # Test same plan, increased users owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=10) - desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 14} + plan = Plan.objects.get(name=PlanName.CODECOV_PRO_YEARLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "always_invoice" ) # Test same plan, decrease users owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) - desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 14} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "none" + ) # Test going from monthly to yearly owner = OwnerFactory( plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 ) - desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 14} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=14 + ) + == "always_invoice" ) # monthly to Sentry monthly plan owner = OwnerFactory( plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 ) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 19} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 20} + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" ) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 21} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" ) # yearly to Sentry monthly plan owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 19} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 20} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 21} + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" + ) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "none" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" ) # monthly to Sentry monthly plan owner = OwnerFactory( plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 ) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 19} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 20} + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" ) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 21} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" ) # yearly to Sentry yearly plan owner = OwnerFactory(plan=PlanName.CODECOV_PRO_YEARLY.value, plan_user_count=20) - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 19} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 20} + plan = Plan.objects.get(name=PlanName.SENTRY_YEARLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "none" ) - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 21} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" + ) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" ) # monthly to Sentry yearly plan owner = OwnerFactory( plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=20 ) - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 19} + plan = Plan.objects.get(name=PlanName.SENTRY_YEARLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=19 + ) + == "always_invoice" ) - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 20} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=20 + ) + == "always_invoice" ) - desired_plan = {"value": PlanName.SENTRY_YEARLY.value, "quantity": 21} assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=21 + ) + == "always_invoice" ) # Team to Sentry owner = OwnerFactory(plan=PlanName.TEAM_MONTHLY.value, plan_user_count=10) - desired_plan = {"value": PlanName.SENTRY_MONTHLY.value, "quantity": 10} + plan = Plan.objects.get(name=PlanName.SENTRY_MONTHLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "always_invoice" ) # Team to Pro owner = OwnerFactory(plan=PlanName.TEAM_MONTHLY.value, plan_user_count=10) - desired_plan = {"value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": 10} + plan = Plan.objects.get(name=PlanName.CODECOV_PRO_MONTHLY.value) assert ( - self.stripe._get_proration_params(owner, desired_plan) == "always_invoice" + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "always_invoice" ) # Sentry to Team owner = OwnerFactory(plan=PlanName.SENTRY_MONTHLY.value, plan_user_count=10) - desired_plan = {"value": PlanName.TEAM_MONTHLY.value, "quantity": 10} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" + plan = Plan.objects.get(name=PlanName.TEAM_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "none" + ) # Sentry to Pro owner = OwnerFactory( plan=PlanName.CODECOV_PRO_MONTHLY.value, plan_user_count=10 ) - desired_plan = {"value": PlanName.TEAM_MONTHLY.value, "quantity": 10} - assert self.stripe._get_proration_params(owner, desired_plan) == "none" + plan = Plan.objects.get(name=PlanName.TEAM_MONTHLY.value) + assert ( + self.stripe._get_proration_params( + owner=owner, desired_plan_info=plan, desired_quantity=10 + ) + == "none" + ) @patch("services.billing.stripe.checkout.Session.create") def test_create_checkout_session_with_no_stripe_customer_id( @@ -1515,6 +1617,7 @@ def test_create_checkout_session_with_no_stripe_customer_id( "value": PlanName.CODECOV_PRO_MONTHLY.value, "quantity": desired_quantity, } + plan = Plan.objects.get(name=desired_plan["value"]) assert self.stripe.create_checkout_session(owner, desired_plan) == expected_id @@ -1529,7 +1632,7 @@ def test_create_checkout_session_with_no_stripe_customer_id( mode="subscription", line_items=[ { - "price": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "price": plan.stripe_id, "quantity": desired_quantity, } ], @@ -1566,6 +1669,8 @@ def test_create_checkout_session_with_stripe_customer_id( assert self.stripe.create_checkout_session(owner, desired_plan) == expected_id + plan = Plan.objects.get(name=desired_plan["value"]) + create_checkout_session_mock.assert_called_once_with( billing_address_collection="required", payment_method_configuration=settings.STRIPE_PAYMENT_METHOD_CONFIGURATION_ID, @@ -1577,7 +1682,7 @@ def test_create_checkout_session_with_stripe_customer_id( mode="subscription", line_items=[ { - "price": settings.STRIPE_PLAN_IDS[desired_plan["value"]], + "price": plan.stripe_id, "quantity": desired_quantity, } ], @@ -1595,6 +1700,32 @@ def test_create_checkout_session_with_stripe_customer_id( customer_update={"name": "auto", "address": "auto"}, ) + @patch("logging.Logger.error") + @patch("services.billing.stripe.checkout.Session.create") + def test_create_checkout_session_with_invalid_plan( + self, create_checkout_session_mock, logger_error_mock + ): + stripe_customer_id = "test-cusa78723hb4@" + owner = OwnerFactory( + service=Service.GITHUB.value, + stripe_customer_id=stripe_customer_id, + ) + desired_quantity = 25 + desired_plan = { + "value": "invalid_plan", + "quantity": desired_quantity, + } + + self.stripe.create_checkout_session(owner, desired_plan) + + create_checkout_session_mock.assert_not_called() + logger_error_mock.assert_called_once_with( + f"Plan {desired_plan['value']} not found", + extra=dict( + owner_id=owner.ownerid, + ), + ) + def test_get_subscription_when_no_subscription(self): owner = OwnerFactory(stripe_subscription_id=None) assert self.stripe.get_subscription(owner) is None