Skip to content

Commit

Permalink
update account-detail endpoint to show info on Account if the Org has…
Browse files Browse the repository at this point in the history
… an Account (#730)
  • Loading branch information
nora-codecov authored Aug 5, 2024
1 parent e245d32 commit 96ade56
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 7 deletions.
41 changes: 36 additions & 5 deletions api/internal/owner/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,11 @@ def validate_value(self, value):
return value

def validate(self, plan):
owner = self.context["view"].owner
current_org = self.context["view"].owner
if current_org.account:
raise serializers.ValidationError(
detail="You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io"
)

# Validate quantity here because we need access to whole plan object
if plan["value"] in PAID_PLANS:
Expand All @@ -156,16 +160,19 @@ def validate(self, plan):
"Quantity for paid plan must be greater than 1"
)

plan_service = PlanService(current_org=owner)
plan_service = PlanService(current_org=current_org)
is_org_trialing = plan_service.is_org_trialing

if plan["quantity"] < owner.activated_user_count and not is_org_trialing:
if (
plan["quantity"] < current_org.activated_user_count
and not is_org_trialing
):
raise serializers.ValidationError(
"Quantity cannot be lower than currently activated user count"
)
if (
plan["quantity"] == owner.plan_user_count
and plan["value"] == owner.plan
plan["quantity"] == current_org.plan_user_count
and plan["value"] == current_org.plan
and not is_org_trialing
):
raise serializers.ValidationError(
Expand Down Expand Up @@ -252,6 +259,10 @@ class AccountDetailsSerializer(serializers.ModelSerializer):
root_organization = RootOrganizationSerializer()
schedule_detail = serializers.SerializerMethodField()
apply_cancellation_discount = serializers.BooleanField(write_only=True)
activated_student_count = serializers.SerializerMethodField()
activated_user_count = serializers.SerializerMethodField()
delinquent = serializers.SerializerMethodField()
uses_invoice = serializers.SerializerMethodField()

class Meta:
model = Owner
Expand Down Expand Up @@ -296,6 +307,26 @@ def get_schedule_detail(self, owner):
def get_checkout_session_id(self, _):
return self.context.get("checkout_session_id")

def get_activated_student_count(self, owner):
if owner.account:
return owner.account.activated_student_count
return owner.activated_student_count

def get_activated_user_count(self, owner):
if owner.account:
return owner.account.activated_user_count
return owner.activated_user_count

def get_delinquent(self, owner):
if owner.account:
return owner.account.is_delinquent
return owner.delinquent

def get_uses_invoice(self, owner):
if owner.account:
return owner.account.invoice_billing.filter(is_active=True).exists()
return owner.uses_invoice

def update(self, instance, validated_data):
if "pretty_plan" in validated_data:
desired_plan = validated_data.pop("pretty_plan")
Expand Down
9 changes: 9 additions & 0 deletions api/internal/owner/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound, PermissionDenied, ValidationError
from rest_framework.response import Response
from shared.django_apps.codecov_auth.models import Owner

from api.shared.mixins import OwnerPropertyMixin
from api.shared.owner.mixins import OwnerViewSetMixin, UserViewSetMixin
Expand Down Expand Up @@ -57,6 +58,14 @@ def destroy(self, request, *args, **kwargs):
return Response(status=status.HTTP_204_NO_CONTENT)

def get_object(self):
if self.owner.account:
# gets the related account and invoice_billing objects from db in 1 query
# otherwise, each reference to owner.account would be an additional query
self.owner = (
Owner.objects.filter(pk=self.owner.ownerid)
.select_related("account__invoice_billing")
.first()
)
return self.owner

@action(detail=False, methods=["patch"])
Expand Down
164 changes: 164 additions & 0 deletions api/internal/tests/views/test_account_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from shared.django_apps.codecov_auth.tests.factories import (
AccountFactory,
InvoiceBillingFactory,
StripeBillingFactory,
)
from stripe import StripeError

from api.internal.tests.test_utils import GetAdminProviderAdapter
Expand Down Expand Up @@ -655,6 +660,26 @@ def test_update_can_set_plan_auto_activate_to_false(self):
assert self.current_owner.plan_auto_activate is False
assert response.data["plan_auto_activate"] is False

def test_update_can_set_plan_auto_activate_on_org_with_account(self):
self.current_owner.account = AccountFactory()
self.current_owner.plan_auto_activate = True
self.current_owner.save()

response = self._update(
kwargs={
"service": self.current_owner.service,
"owner_username": self.current_owner.username,
},
data={"plan_auto_activate": False},
)

assert response.status_code == status.HTTP_200_OK

self.current_owner.refresh_from_db()

assert self.current_owner.plan_auto_activate is False
assert response.data["plan_auto_activate"] is False

def test_update_can_set_plan_to_users_basic(self):
self.current_owner.plan = PlanName.CODECOV_PRO_YEARLY_LEGACY.value
self.current_owner.save()
Expand Down Expand Up @@ -938,6 +963,48 @@ def test_update_must_fail_if_team_plan_and_too_many_users(self):
== "Quantity for Team plan cannot exceed 10"
)

def test_update_quantity_must_fail_if_account(self):
desired_plans = [
{"quantity": 10},
]
self.current_owner.account = AccountFactory()
self.current_owner.save()
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 (
str(response.data["plan"]["non_field_errors"][0])
== "You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io"
)

def test_update_plan_must_fail_if_account(self):
desired_plans = [
{"value": PlanName.CODECOV_PRO_YEARLY.value},
]
self.current_owner.account = AccountFactory()
self.current_owner.save()
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 (
str(response.data["plan"]["non_field_errors"][0])
== "You cannot update your plan manually, for help or changes to plan, connect with sales@codecov.io"
)

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 @@ -1499,6 +1566,103 @@ def test_destroy_not_own_account_returns_404(self):
)
assert response.status_code == status.HTTP_404_NOT_FOUND

def test_retrieve_org_with_account(self):
account = AccountFactory(
name="Hello World",
plan_seat_count=5,
free_seat_count=3,
plan="users-enterprisey",
is_delinquent=False,
)
InvoiceBillingFactory(is_active=True, account=account)
org_1 = OwnerFactory(
account=account,
service=Service.GITHUB.value,
username="Test",
delinquent=True,
uses_invoice=False,
)
org_2 = OwnerFactory(
account=account,
service=Service.GITHUB.value,
)
activated_owner = OwnerFactory(
user=UserFactory(), organizations=[org_1.ownerid, org_2.ownerid]
)
account.users.add(activated_owner.user)
student_owner = OwnerFactory(
user=UserFactory(),
student=True,
organizations=[org_1.ownerid, org_2.ownerid],
)
account.users.add(student_owner.user)
other_activated_owner = OwnerFactory(
user=UserFactory(), organizations=[org_2.ownerid]
)
account.users.add(other_activated_owner.user)
other_student_owner = OwnerFactory(
user=UserFactory(),
student=True,
organizations=[org_2.ownerid],
)
account.users.add(other_student_owner.user)
org_1.plan_activated_users = [activated_owner.ownerid, student_owner.ownerid]
org_1.admins = [activated_owner.ownerid]
org_1.save()
org_2.plan_activated_users = [
activated_owner.ownerid,
student_owner.ownerid,
other_activated_owner.ownerid,
other_student_owner.ownerid,
]
org_2.save()

self.client.force_login_owner(activated_owner)
response = self._retrieve(
kwargs={"service": Service.GITHUB.value, "owner_username": org_1.username}
)
assert response.status_code == status.HTTP_200_OK
# these fields are all overridden by account fields if the org has an account
self.assertEqual(org_1.activated_user_count, 1)
self.assertEqual(org_1.activated_student_count, 1)
self.assertTrue(org_1.delinquent)
self.assertFalse(org_1.uses_invoice)
self.assertEqual(org_1.plan_user_count, 1)
expected_response = {
"activated_user_count": 2,
"activated_student_count": 2,
"delinquent": False,
"uses_invoice": True,
"plan": {
"marketing_name": "Enterprise Cloud",
"value": PlanName.ENTERPRISE_CLOUD_YEARLY.value,
"billing_rate": "annually",
"base_unit_price": 10,
"benefits": [
"Configurable # of users",
"Unlimited public repositories",
"Unlimited private repositories",
"Priority Support",
],
"quantity": 5,
},
"root_organization": None,
"integration_id": org_1.integration_id,
"plan_auto_activate": org_1.plan_auto_activate,
"inactive_user_count": 0,
"subscription_detail": None,
"checkout_session_id": None,
"name": org_1.name,
"email": org_1.email,
"nb_active_private_repos": 0,
"repo_total_credits": 99999999,
"plan_provider": org_1.plan_provider,
"student_count": 1,
"schedule_detail": None,
}
self.assertDictEqual(response.data["plan"], expected_response["plan"])
self.assertDictEqual(response.data, expected_response)


@override_settings(IS_ENTERPRISE=True)
class EnterpriseAccountViewSetTests(APITestCase):
Expand Down
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ factory-boy
fakeredis
freezegun
https://github.com/codecov/opentelem-python/archive/refs/tags/v0.0.4a1.tar.gz#egg=codecovopentelem
https://github.com/codecov/shared/archive/cee7681d68ce0d9f57899d41a6241d0772b07905.tar.gz#egg=shared
https://github.com/codecov/shared/archive/14d8093f85adf1b99df763354c10ec0eaf86ee43.tar.gz#egg=shared
google-cloud-pubsub
gunicorn>=22.0.0
https://github.com/photocrowd/django-cursor-pagination/archive/f560902696b0c8509e4d95c10ba0d62700181d84.tar.gz
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ sentry-sdk[celery]==1.44.1
# shared
setproctitle==1.1.10
# via -r requirements.in
shared @ https://github.com/codecov/shared/archive/cee7681d68ce0d9f57899d41a6241d0772b07905.tar.gz
shared @ https://github.com/codecov/shared/archive/14d8093f85adf1b99df763354c10ec0eaf86ee43.tar.gz
# via -r requirements.in
simplejson==3.17.2
# via -r requirements.in
Expand Down

0 comments on commit 96ade56

Please sign in to comment.