Skip to content

Commit

Permalink
feat: adding subsidy info to policy model (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnagro authored Aug 11, 2023
2 parents ef3c269 + e081603 commit 47c8a2d
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ http://localhost:18270/api/schema/redoc/#tag/Subsidy-Access-Policies-CRUD/operat

A ``POST`` request to ``/api/v1/subsidy-access-policies/`` will create, idempotently, a policy record - if an equivalent
record already exists, that record will be returned and no new policy is created. Use this
endpoint to create a new policy record, preferrably after the related subsidy and catalog
endpoint to create a new policy record, preferably after the related subsidy and catalog
records already exist.

Policy Retrieval
Expand Down
35 changes: 34 additions & 1 deletion enterprise_access/apps/api/serializers/subsidy_access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class Meta:
'per_learner_enrollment_limit',
'per_learner_spend_limit',
'spend_limit',
'subsidy_active_datetime',
'subsidy_expiration_datetime',
'is_subsidy_active',
]
read_only_fields = fields

Expand Down Expand Up @@ -79,6 +82,9 @@ class Meta:
'per_learner_enrollment_limit',
'per_learner_spend_limit',
'spend_limit',
'subsidy_active_datetime',
'subsidy_expiration_datetime',
'is_subsidy_active',
]
read_only_fields = ['uuid']
extra_kwargs = {
Expand Down Expand Up @@ -114,6 +120,18 @@ class Meta:
'allow_null': True,
'required': False,
},
'subsidy_active_datetime': {
'allow_null': True,
'required': False,
},
'subsidy_expiration_datetime': {
'allow_null': True,
'required': False,
},
'is_subsidy_active': {
'allow_null': True,
'required': False,
},
}

@property
Expand Down Expand Up @@ -273,6 +291,9 @@ class Meta:
'spend_limit',
'per_learner_spend_limit',
'per_learner_enrollment_limit',
'subsidy_active_datetime',
'subsidy_expiration_datetime',
'is_subsidy_active',
)
extra_kwargs = {
'description': {
Expand Down Expand Up @@ -310,12 +331,24 @@ class Meta:
'allow_null': True,
'required': False,
},
'subsidy_active_datetime': {
'allow_null': True,
'required': False,
},
'subsidy_expiration_datetime': {
'allow_null': True,
'required': False,
},
'is_subsidy_active': {
'allow_null': True,
'required': False,
},
}

def validate(self, attrs):
"""
Raises a ValidationError if any field not explicitly declared
as a field in this serializer defintion is provided as input.
as a field in this serializer definition is provided as input.
"""
unknown = sorted(set(self.initial_data) - set(self.fields))
if unknown:
Expand Down
55 changes: 54 additions & 1 deletion enterprise_access/apps/api/v1/tests/test_subsidy_access_policy_views.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""
Tests for Enterprise Access Subsidy Access Policy app API v1 views.
"""
from datetime import datetime, timedelta
from operator import itemgetter
from unittest import mock
from unittest.mock import patch
from uuid import UUID, uuid4

import ddt
Expand All @@ -22,6 +24,7 @@
PolicyTypes,
TransactionStateChoices
)
from enterprise_access.apps.subsidy_access_policy.models import SubsidyAccessPolicy
from enterprise_access.apps.subsidy_access_policy.tests.factories import (
PerLearnerEnrollmentCapLearnerCreditAccessPolicyFactory,
PerLearnerSpendCapLearnerCreditAccessPolicyFactory
Expand Down Expand Up @@ -62,6 +65,26 @@ def setUp(self):
# Start in an unauthenticated state.
self.client.logout()

def setup_subsidy_mocks(self):
"""
Setup mocks for subsidy.
"""
self.yesterday = datetime.utcnow() - timedelta(days=1)
self.tomorrow = datetime.utcnow() + timedelta(days=1)
mock_subsidy = {
'id': 123455,
'active_datetime': self.yesterday,
'expiration_datetime': self.tomorrow,
'is_active': True,
}
subsidy_client_patcher = patch.object(
SubsidyAccessPolicy, 'subsidy_client'
)
self.mock_subsidy_client = subsidy_client_patcher.start()
self.mock_subsidy_client.retrieve_subsidy.return_value = mock_subsidy

self.addCleanup(subsidy_client_patcher.stop)


@ddt.ddt
class TestPolicyCRUDAuthNAndPermissionChecks(CRUDViewTestMixin, APITestWithMocks):
Expand Down Expand Up @@ -201,6 +224,11 @@ class TestAuthenticatedPolicyCRUDViews(CRUDViewTestMixin, APITestWithMocks):
"""
Test the list and detail views for subsidy access policy records.
"""

def setUp(self):
super().setUp()
super().setup_subsidy_mocks()

@ddt.data(
# A good admin role, but for a context/customer that doesn't match anything we're aware of, gets you a 403.
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': str(TEST_ENTERPRISE_UUID)},
Expand Down Expand Up @@ -233,6 +261,9 @@ def test_detail_view(self, role_context_dict):
'spend_limit': 3,
'subsidy_uuid': str(self.redeemable_policy.subsidy_uuid),
'uuid': str(self.redeemable_policy.uuid),
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
}, response.json())

@ddt.data(
Expand Down Expand Up @@ -272,6 +303,9 @@ def test_list_view(self, role_context_dict):
'spend_limit': 0,
'subsidy_uuid': str(self.non_redeemable_policy.subsidy_uuid),
'uuid': str(self.non_redeemable_policy.uuid),
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
},
{
'access_method': 'direct',
Expand All @@ -285,6 +319,9 @@ def test_list_view(self, role_context_dict):
'spend_limit': 3,
'subsidy_uuid': str(self.redeemable_policy.subsidy_uuid),
'uuid': str(self.redeemable_policy.uuid),
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
},
]

Expand Down Expand Up @@ -341,6 +378,9 @@ def test_destroy_view(self, request_payload, expected_change_reason):
'spend_limit': 3,
'subsidy_uuid': str(self.redeemable_policy.subsidy_uuid),
'uuid': str(self.redeemable_policy.uuid),
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
}
self.assertEqual(expected_response, response.json())

Expand Down Expand Up @@ -405,6 +445,9 @@ def test_update_views(self, is_patch):
'spend_limit': request_payload['spend_limit'],
'subsidy_uuid': request_payload['subsidy_uuid'],
'uuid': str(policy_for_edit.uuid),
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
}
self.assertEqual(expected_response, response.json())

Expand Down Expand Up @@ -511,6 +554,10 @@ class TestAdminPolicyCreateView(CRUDViewTestMixin, APITestWithMocks):
``SubsidyAccessPolicyViewSet`` implementation.
"""

def setUp(self):
super().setUp()
super().setup_subsidy_mocks()

@ddt.data(
{
'policy_type': PolicyTypes.PER_LEARNER_ENROLLMENT_CREDIT,
Expand Down Expand Up @@ -603,6 +650,9 @@ def test_create_view(self, policy_type, extra_fields, expected_response_code, ex
'subsidy_uuid': str(uuid4()),
'access_method': AccessMethods.DIRECT,
'spend_limit': None,
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
}
payload.update(extra_fields)
response = self.client.post(SUBSIDY_ACCESS_POLICY_LIST_ENDPOINT, payload)
Expand Down Expand Up @@ -654,6 +704,9 @@ def test_idempotent_create_view(self, policy_type, extra_fields, expected_respon
'subsidy_uuid': subsidy_uuid,
'access_method': AccessMethods.DIRECT,
'spend_limit': None,
'subsidy_active_datetime': self.yesterday.isoformat(),
'subsidy_expiration_datetime': self.tomorrow.isoformat(),
'is_subsidy_active': True,
}
payload.update(extra_fields)
response = self.client.post(SUBSIDY_ACCESS_POLICY_LIST_ENDPOINT, payload)
Expand Down Expand Up @@ -1489,7 +1542,7 @@ def test_can_redeem_policy_existing_reversed_redemptions(self, mock_transactions
@mock.patch('enterprise_access.apps.api.v1.views.subsidy_access_policy.LmsApiClient')
def test_can_redeem_policy_no_price(self, mock_lms_client, mock_transactions_cache_for_learner):
"""
Test that the can_redeem endpoint successfuly serializes a response for content that has no price.
Test that the can_redeem endpoint successfully serializes a response for content that has no price.
"""
test_content_key = "course-v1:demox+1234+2T2023"
mock_lms_client.return_value.get_enterprise_customer_data.return_value = {
Expand Down
4 changes: 2 additions & 2 deletions enterprise_access/apps/api/v1/views/subsidy_access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,8 +397,8 @@ def redeem(self, request, *args, **kwargs):
400: There are missing or otherwise invalid input parameters.
403: The requester has insufficient redeem permissions.
422: The subisdy access policy is not redeemable in a way that IS NOT retryable.
429: The subisdy access policy is not redeemable in a way that IS retryable (e.g. policy currently locked).
422: The subsidy access policy is not redeemable in a way that IS NOT retryable.
429: The subsidy access policy is not redeemable in a way that IS retryable (e.g. policy currently locked).
200: The policy was successfully redeemed. Response body is JSON with a serialized Transaction
containing the following keys (sample values):
{
Expand Down
2 changes: 1 addition & 1 deletion enterprise_access/apps/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def health(_):
'{"overall_status": "OK", "detailed_status": {"database_status": "OK", "lms_status": "OK"}}'
"""

# Ignores health check in performance monitoring so as to not artifically inflate our response time metrics
# Ignores health check in performance monitoring so as to not artificially inflate our response time metrics
ignore_transaction()

try:
Expand Down
24 changes: 24 additions & 0 deletions enterprise_access/apps/subsidy_access_policy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,30 @@ class SubsidyAccessPolicy(TimeStampedModel):
# ProxyAwareHistoricalRecords docstring for more info.
history = ProxyAwareHistoricalRecords(inherit=True)

@property
def subsidy_active_datetime(self):
"""
The datetime when this policy's associated subsidy is considered active.
If null, this subsidy is considered active.
"""
return self.subsidy_record().get('active_datetime')

@property
def subsidy_expiration_datetime(self):
"""
The datetime when this policy's associated subsidy is considered expired.
If null, this subsidy is considered active.
"""
return self.subsidy_record().get('expiration_datetime')

@property
def is_subsidy_active(self):
"""
Returns true if the localized current time is
between ``subsidy_active_datetime`` and ``subsidy_expiration_datetime``.
"""
return self.subsidy_record().get('is_active')

@property
def subsidy_client(self):
"""
Expand Down
18 changes: 18 additions & 0 deletions enterprise_access/apps/subsidy_access_policy/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for subsidy_access_policy models.
"""
from datetime import datetime, timedelta
from unittest.mock import patch
from uuid import uuid4

Expand Down Expand Up @@ -539,3 +540,20 @@ def test_content_would_exceed_limit_positive_spent_amount(self):
"""
with self.assertRaisesRegex(Exception, 'Expected a sum of transaction quantities <= 0'):
self.per_learner_enroll_policy.content_would_exceed_limit(10, 100, 15)

def test_mock_subsidy_datetimes(self):
yesterday = datetime.utcnow() - timedelta(days=1)
tomorrow = datetime.utcnow() + timedelta(days=1)
mock_subsidy = {
'id': 123455,
'active_datetime': yesterday,
'expiration_datetime': tomorrow,
'is_active': True,
}
self.mock_subsidy_client.retrieve_subsidy.return_value = mock_subsidy
policy = PerLearnerEnrollmentCapLearnerCreditAccessPolicyFactory.create()
assert policy.subsidy_record() == mock_subsidy

assert policy.subsidy_active_datetime == mock_subsidy.get('active_datetime')
assert policy.subsidy_expiration_datetime == mock_subsidy.get('expiration_datetime')
assert policy.is_subsidy_active == mock_subsidy.get('is_active')

0 comments on commit 47c8a2d

Please sign in to comment.