Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Modify billing for Team plan (#637) #225

Merged
merged 26 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
34093a3
feat: Modify billing for Team plan
JerrySentry Nov 2, 2023
73ca159
add unit test
JerrySentry Nov 2, 2023
8f0a5cf
fix: available plans for users while trialing
JerrySentry Nov 3, 2023
9e55518
linter fix
JerrySentry Nov 3, 2023
a876186
update unit tests
JerrySentry Nov 3, 2023
99034a8
plan_activated_users=None
JerrySentry Nov 3, 2023
825789b
feat: Add Sentry user to admin panel (#702)
JerrySentry Nov 6, 2023
9e5f09e
Revert "feat: Add Sentry user to admin panel (#702)"
JerrySentry Nov 6, 2023
f7f3863
add team plan stripe IDs
JerrySentry Nov 6, 2023
303a4d0
Merge branch 'api_731' into api_637
JerrySentry Nov 6, 2023
250fde4
update to use new product IDs
JerrySentry Nov 7, 2023
73c6d9c
Merge branch 'main' into api_637
JerrySentry Nov 7, 2023
3b5a442
Merge branch 'api_637' of github.com:codecov/codecov-api into api_637
JerrySentry Nov 7, 2023
0604356
delete /internal/plans endpoint and refactor plan validation
JerrySentry Nov 7, 2023
c95aba7
Merge branch 'main' into api_637
JerrySentry Nov 7, 2023
e9bba42
add a unit test
JerrySentry Nov 7, 2023
829e81b
Merge branch 'main' into api_637
JerrySentry Nov 7, 2023
e7f577b
s/repositories/uploads
JerrySentry Nov 7, 2023
4f5ea9c
unit test change
JerrySentry Nov 8, 2023
206841a
fix: Add isPublic filter to measurements filter
JerrySentry Nov 9, 2023
628052e
fix: s/Pro Team/Pro
JerrySentry Nov 9, 2023
9f31fc7
fix: rework logic for available plans from seats to activated users f…
JerrySentry Nov 9, 2023
4c0a519
Merge branch 'main' into api_637
JerrySentry Nov 9, 2023
7af18da
add propration logic for team plan
JerrySentry Nov 9, 2023
43b3214
address CR comments
JerrySentry Nov 9, 2023
dfbf514
Merge branch 'main' into api_637
JerrySentry Nov 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions api/internal/owner/serializers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import logging
from dataclasses import asdict
from datetime import datetime

from dateutil.relativedelta import relativedelta
from django.conf import settings
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied

from billing.helpers import available_plans
from codecov_auth.models import Owner
from plan.constants import PRO_PLANS, SENTRY_PAID_USER_PLAN_REPRESENTATIONS
from plan.constants import (
PAID_PLANS,
SENTRY_PAID_USER_PLAN_REPRESENTATIONS,
TEAM_PLAN_MAX_USERS,
TEAM_PLAN_REPRESENTATIONS,
)
from plan.service import PlanService
from services.billing import BillingService
from services.sentry import send_user_webhook as send_sentry_webhook
Expand Down Expand Up @@ -117,8 +122,14 @@ class PlanSerializer(serializers.Serializer):
quantity = serializers.IntegerField(required=False)

def validate_value(self, value):
current_org = self.context["view"].owner
current_owner = self.context["request"].current_owner
plan_values = [plan["value"] for plan in available_plans(current_owner)]

plan_service = PlanService(current_org=current_org)
available_plans = [
asdict(plan) for plan in plan_service.available_plans(current_owner)
]
plan_values = [plan["value"] for plan in available_plans]
if value not in plan_values:
if value in SENTRY_PAID_USER_PLAN_REPRESENTATIONS:
log.warning(
Expand All @@ -134,7 +145,7 @@ def validate(self, plan):
owner = self.context["view"].owner

# Validate quantity here because we need access to whole plan object
if plan["value"] in PRO_PLANS:
if plan["value"] in PAID_PLANS:
if "quantity" not in plan:
raise serializers.ValidationError(
f"Field 'quantity' required for updating to paid plans"
Expand All @@ -159,6 +170,13 @@ def validate(self, plan):
raise serializers.ValidationError(
f"Quantity or plan for paid plan must be different from the existing one"
)
if (
plan["value"] in TEAM_PLAN_REPRESENTATIONS
and plan["quantity"] > TEAM_PLAN_MAX_USERS
):
raise serializers.ValidationError(
f"Quantity for Team plan cannot exceed {TEAM_PLAN_MAX_USERS}"
)
return plan


Expand All @@ -185,7 +203,7 @@ def get_plan(self, phase):
plan_name = list(stripe_plan_dict.keys())[
list(stripe_plan_dict.values()).index(plan_id)
]
marketing_plan_name = PRO_PLANS[plan_name].billing_rate
marketing_plan_name = PAID_PLANS[plan_name].billing_rate
return marketing_plan_name

def get_quantity(self, phase):
Expand Down
11 changes: 2 additions & 9 deletions api/internal/owner/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from dataclasses import asdict

from django.db.models import F
from django_filters import rest_framework as django_filters
Expand All @@ -10,7 +11,7 @@
from api.shared.mixins import OwnerPropertyMixin
from api.shared.owner.mixins import OwnerViewSetMixin, UserViewSetMixin
from api.shared.permissions import MemberOfOrgPermissions
from billing.helpers import available_plans, on_enterprise_plan
from billing.helpers import on_enterprise_plan
from services.billing import BillingService
from services.decorators import stripe_safe
from services.task import TaskService
Expand Down Expand Up @@ -151,11 +152,3 @@ def get_queryset(self):
# pull ordering only available for enterprise
qs = qs.annotate_last_pull_timestamp()
return qs


class PlanViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
def get_queryset(self):
return None

def list(self, request, *args, **kwargs):
return Response(available_plans(request.current_owner))
109 changes: 107 additions & 2 deletions api/internal/tests/views/test_account_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from api.internal.tests.test_utils import GetAdminProviderAdapter
from codecov_auth.models import Service
from codecov_auth.tests.factories import OwnerFactory, UserFactory
from plan.constants import PlanName
from plan.constants import PlanName, TrialStatus
from utils.test_utils import APIClient

curr_path = os.path.dirname(__file__)
Expand Down Expand Up @@ -857,6 +857,111 @@ def test_update_must_fail_if_quantity_and_plan_are_equal_to_the_owners_current_o
== "Quantity or plan for paid plan must be different from the existing one"
)

def test_update_team_plan_must_fail_if_not_trialing(self):
JerrySentry marked this conversation as resolved.
Show resolved Hide resolved
self.current_owner.plan = PlanName.BASIC_PLAN_NAME.value
self.current_owner.plan_user_count = 1
JerrySentry marked this conversation as resolved.
Show resolved Hide resolved
self.current_owner.save()
desired_plans = [
{"value": PlanName.TEAM_MONTHLY.value, "quantity": 1},
{"value": PlanName.TEAM_YEARLY.value, "quantity": 1},
]

for desired_plan in desired_plans:
response = self._update(
kwargs={
"service": self.current_owner.service,
"owner_username": self.current_owner.username,
},
data={"plan": desired_plan},
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"plan": {
"value": [
f"Invalid value for plan: {desired_plan['value']}; must be one of ['users-basic', 'users-pr-inappm', 'users-pr-inappy']"
]
}
}

def test_update_team_plan_must_fail_if_too_many_activated_users_during_trial(self):
self.current_owner.plan = PlanName.BASIC_PLAN_NAME.value
self.current_owner.plan_user_count = 1
self.current_owner.trial_status = TrialStatus.ONGOING.value
self.current_owner.plan_activated_users = [i for i in range(11)]
self.current_owner.save()

desired_plans = [
{"value": PlanName.TEAM_MONTHLY.value, "quantity": 10},
{"value": PlanName.TEAM_YEARLY.value, "quantity": 10},
]

for desired_plan in desired_plans:
response = self._update(
kwargs={
"service": self.current_owner.service,
"owner_username": self.current_owner.username,
},
data={"plan": desired_plan},
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"plan": {
"value": [
f"Invalid value for plan: {desired_plan['value']}; must be one of ['users-basic', 'users-pr-inappm', 'users-pr-inappy']"
]
}
}

def test_update_team_plan_must_fail_if_currently_team_plan_add_too_many_users(self):
self.current_owner.plan = PlanName.TEAM_MONTHLY.value
self.current_owner.plan_user_count = 1
self.current_owner.save()

desired_plans = [
{"value": PlanName.TEAM_MONTHLY.value, "quantity": 11},
{"value": PlanName.TEAM_YEARLY.value, "quantity": 11},
]

for desired_plan in desired_plans:
response = self._update(
kwargs={
"service": self.current_owner.service,
"owner_username": self.current_owner.username,
},
data={"plan": desired_plan},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert (
response.data["plan"]["non_field_errors"][0]
== "Quantity for Team plan cannot exceed 10"
)

def test_update_must_fail_if_team_plan_and_too_many_users(self):
JerrySentry marked this conversation as resolved.
Show resolved Hide resolved
desired_plans = [
{"value": PlanName.TEAM_MONTHLY.value, "quantity": 11},
{"value": PlanName.TEAM_YEARLY.value, "quantity": 11},
]

for desired_plan in desired_plans:
response = self._update(
kwargs={
"service": self.current_owner.service,
"owner_username": self.current_owner.username,
},
data={"plan": desired_plan},
)

assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"plan": {
"value": [
f"Invalid value for plan: {desired_plan['value']}; must be one of ['users-basic', 'users-pr-inappm', 'users-pr-inappy']"
]
}
}

def test_update_quantity_must_be_at_least_2_if_paid_plan(self):
desired_plan = {"value": PlanName.CODECOV_PRO_YEARLY.value, "quantity": 1}
response = self._update(
Expand Down Expand Up @@ -1109,7 +1214,7 @@ def test_update_sentry_plan_non_sentry_user(
assert res.json() == {
"plan": {
"value": [
"Invalid value for plan: users-sentrym; must be one of ['users-free', 'users-basic', 'users-pr-inappm', 'users-pr-inappy']"
"Invalid value for plan: users-sentrym; must be one of ['users-basic', 'users-pr-inappm', 'users-pr-inappy']"
JerrySentry marked this conversation as resolved.
Show resolved Hide resolved
]
}
}
Expand Down
Loading
Loading