Skip to content

Commit

Permalink
feat: can-redeem user message for no/cancelled/errored assignments
Browse files Browse the repository at this point in the history
ENT-8114 | The can-redeem endpoint now returns a reason and
user message related to policies that are unredeemable due
to a lack of allocated `LearnerContentAssignment` record
for the requesting user, or an assignment in a cancelled or errored state.
  • Loading branch information
iloveagent57 committed Dec 11, 2023
1 parent c6c2033 commit 9c476d2
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
SYSTEM_ENTERPRISE_OPERATOR_ROLE
)
from enterprise_access.apps.subsidy_access_policy.constants import (
REASON_LEARNER_ASSIGNMENT_CANCELLED,
REASON_LEARNER_ASSIGNMENT_FAILED,
REASON_LEARNER_NOT_ASSIGNED_CONTENT,
REASON_NOT_ENOUGH_VALUE_IN_SUBSIDY,
AccessMethods,
MissingSubsidyAccessReasonUserMessages,
Expand Down Expand Up @@ -2028,6 +2031,26 @@ def setUp(self):
content_quantity=-self.assigned_price_cents,
state=LearnerContentAssignmentStateChoices.ALLOCATED,
)
self.cancelled_content_key = 'edX+CancelledX'
self.cancelled_assignment = LearnerContentAssignmentFactory.create(
assignment_configuration=self.assignment_configuration,
learner_email='[email protected]',
lms_user_id=self.user.lms_user_id,
content_key=self.cancelled_content_key,
content_title='CANCELLED ASSIGNMENT',
content_quantity=-self.assigned_price_cents,
state=LearnerContentAssignmentStateChoices.CANCELLED,
)
self.failed_content_key = 'edX+FailedX'
self.cancelled_assignment = LearnerContentAssignmentFactory.create(
assignment_configuration=self.assignment_configuration,
learner_email='[email protected]',
lms_user_id=self.user.lms_user_id,
content_key=self.failed_content_key,
content_title='FAILED ASSIGNMENT',
content_quantity=-self.assigned_price_cents,
state=LearnerContentAssignmentStateChoices.ERRORED,
)

@mock.patch('enterprise_access.apps.subsidy_access_policy.subsidy_api.get_and_cache_transactions_for_learner')
def test_can_redeem_assigned_policy(self, mock_transactions_cache_for_learner):
Expand Down Expand Up @@ -2076,3 +2099,90 @@ def test_can_redeem_assigned_policy(self, mock_transactions_cache_for_learner):
str(self.assigned_learner_credit_policy.uuid)
assert response_list[0]["can_redeem"] is True
assert len(response_list[0]["reasons"]) == 0

@mock.patch('enterprise_access.apps.api.v1.views.subsidy_access_policy.LmsApiClient')
@mock.patch('enterprise_access.apps.subsidy_access_policy.subsidy_api.get_and_cache_transactions_for_learner')
@ddt.data(
# Only a cancelled assignment exists.
{'has_cancelled_assignment': True, 'has_failed_assignment': False},
# Only an errored assignment exists.
{'has_cancelled_assignment': False, 'has_failed_assignment': True},
# No assignment exists for the learner/content pair to check.
{'has_cancelled_assignment': False, 'has_failed_assignment': True},
)
@ddt.unpack
def test_can_redeem_no_assignment_for_content(
self, mock_transactions_cache_for_learner, mock_lms_client,
has_cancelled_assignment, has_failed_assignment,
):
"""
Test that the can_redeem endpoint returns appropriate error reasons and user messages
when checking re-deemability of unassigned/cancelled/failed assigned content.
"""
mock_transactions_cache_for_learner.return_value = {
'transactions': [],
'aggregates': {
'total_quantity': 0,
},
}
content_key_for_redemption = "course-v1:Unredeemable+Content+3T2020"
if has_cancelled_assignment:
content_key_for_redemption = f"course-v1:{self.cancelled_content_key}+1T2023"
elif has_failed_assignment:
content_key_for_redemption = f"course-v1:{self.failed_content_key}+1T2023"

content_key_for_redemption_metadata_price = 29900
mock_get_subsidy_content_data = {
"content_uuid": str(uuid4()),
"content_key": content_key_for_redemption,
"source": "edX",
"content_price": content_key_for_redemption_metadata_price,
}
self.mock_get_content_metadata.return_value = mock_get_subsidy_content_data

# It's an unredeemable response, so mock out some admin users to return
mock_lms_client.return_value.get_enterprise_customer_data.return_value = {
'slug': 'sluggy',
'admin_users': [{'email': '[email protected]'}],
}

with mock.patch(
'enterprise_access.apps.subsidy_access_policy.content_metadata_api.get_and_cache_content_metadata',
side_effect=mock_get_subsidy_content_data,
):
query_params = {'content_key': [content_key_for_redemption]}
response = self.client.get(self.subsidy_access_policy_can_redeem_endpoint, query_params)

assert response.status_code == status.HTTP_200_OK
response_list = response.json()

assert len(response_list) == 1

# Check the response for the first content_key given.
assert response_list[0]["content_key"] == content_key_for_redemption
assert response_list[0]["list_price"] is None
assert response_list[0]["redemptions"] == []
assert response_list[0]["has_successful_redemption"] is False
assert response_list[0]["redeemable_subsidy_access_policy"] is None
assert response_list[0]["can_redeem"] is False

expected_reason = REASON_LEARNER_NOT_ASSIGNED_CONTENT
expected_message = MissingSubsidyAccessReasonUserMessages.LEARNER_NOT_ASSIGNED_CONTENT
if has_cancelled_assignment:
expected_reason = REASON_LEARNER_ASSIGNMENT_CANCELLED
expected_message = MissingSubsidyAccessReasonUserMessages.LEARNER_ASSIGNMENT_CANCELED
elif has_failed_assignment:
expected_reason = REASON_LEARNER_ASSIGNMENT_FAILED
expected_message = MissingSubsidyAccessReasonUserMessages.LEARNER_NOT_ASSIGNED_CONTENT

expected_reasons = [
{
"reason": expected_reason,
"user_message": expected_message,
"metadata": {
"enterprise_administrators": [{'email': '[email protected]'}],
},
"policy_uuids": [str(self.assigned_learner_credit_policy.uuid)],
},
]
assert response_list[0]["reasons"] == expected_reasons
6 changes: 6 additions & 0 deletions enterprise_access/apps/api/v1/views/subsidy_access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@
from enterprise_access.apps.events.utils import send_subsidy_redemption_event_to_event_bus
from enterprise_access.apps.subsidy_access_policy.constants import (
REASON_CONTENT_NOT_IN_CATALOG,
REASON_LEARNER_ASSIGNMENT_CANCELLED,
REASON_LEARNER_ASSIGNMENT_FAILED,
REASON_LEARNER_MAX_ENROLLMENTS_REACHED,
REASON_LEARNER_MAX_SPEND_REACHED,
REASON_LEARNER_NOT_ASSIGNED_CONTENT,
REASON_LEARNER_NOT_IN_ENTERPRISE,
REASON_NOT_ENOUGH_VALUE_IN_SUBSIDY,
REASON_POLICY_EXPIRED,
Expand Down Expand Up @@ -138,6 +141,9 @@ def _get_user_message_for_reason(reason_slug, enterprise_admin_users):
REASON_LEARNER_MAX_SPEND_REACHED: MissingSubsidyAccessReasonUserMessages.LEARNER_LIMITS_REACHED,
REASON_LEARNER_MAX_ENROLLMENTS_REACHED: MissingSubsidyAccessReasonUserMessages.LEARNER_LIMITS_REACHED,
REASON_CONTENT_NOT_IN_CATALOG: MissingSubsidyAccessReasonUserMessages.CONTENT_NOT_IN_CATALOG,
REASON_LEARNER_NOT_ASSIGNED_CONTENT: MissingSubsidyAccessReasonUserMessages.LEARNER_NOT_ASSIGNED_CONTENT,
REASON_LEARNER_ASSIGNMENT_CANCELLED: MissingSubsidyAccessReasonUserMessages.LEARNER_ASSIGNMENT_CANCELED,
REASON_LEARNER_ASSIGNMENT_FAILED: MissingSubsidyAccessReasonUserMessages.LEARNER_NOT_ASSIGNED_CONTENT,
}

if reason_slug not in MISSING_SUBSIDY_ACCESS_POLICY_REASONS:
Expand Down
4 changes: 4 additions & 0 deletions enterprise_access/apps/subsidy_access_policy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ class MissingSubsidyAccessReasonUserMessages:
"You can't enroll right now because this course is no longer available in your organization's catalog."
LEARNER_NOT_IN_ENTERPRISE = \
"You can't enroll right now because your account is no longer associated with the organization."
LEARNER_NOT_ASSIGNED_CONTENT = \
"You can't enroll right now because this course is not assigned to you."
LEARNER_ASSIGNMENT_CANCELED = \
"You can't enroll right now right now because your administrator canceled your course assignment."


REASON_POLICY_EXPIRED = "policy_expired"
Expand Down

0 comments on commit 9c476d2

Please sign in to comment.