Skip to content

Commit

Permalink
checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
iloveagent57 committed Jun 27, 2023
1 parent dda40f5 commit 08b78e4
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from uuid import UUID, uuid4

import ddt
import requests
from django.conf import settings
from rest_framework import status
from rest_framework.reverse import reverse
Expand Down Expand Up @@ -682,6 +683,52 @@ def test_redeem_policy(self, mock_transactions_cache_for_learner): # pylint: di
),
)

@mock.patch('enterprise_access.apps.subsidy_access_policy.models.get_and_cache_transactions_for_learner')
@mock.patch('enterprise_access.apps.api.v1.views.subsidy_access_policy.LmsApiClient')
@ddt.data(
{
'subsidy_error_code': 'fulfillment_error',
'subsidy_error_detail': [{'message': 'woozit duplicate order woohoo!'}],
'expected_redeem_error_payload': {
'reason': 'the-reason',
'user-message': 'the-user-message',
'metadata': {
'enterprise_administrators': '[email protected]',
},
},
},
)
def test_redeem_policy_subsidy_api_error(
self, mock_lms_api_client, mock_transactions_cache_for_learner,
subsidy_error_code, subsidy_error_detail, expected_redeem_error_payload
):
"""
Verify that SubsidyAccessPolicyRedeemViewset redeem endpoint returns a well-structured
error response payload when the subsidy API call to redeem/fulfill responds with an error.
"""
mock_lms_api_client().get_enterprise_customer_data.return_value = {
'slug': 'the-slug',
'admin_users': [{'email': '[email protected]'}],
}
self.mock_get_content_metadata.return_value = {'content_price': 123}
mock_response = mock.MagicMock()
mock_response.json.return_value = {
'code': subsidy_error_code,
'detail': subsidy_error_detail,
}
self.redeemable_policy.subsidy_client.create_subsidy_transaction.side_effect = requests.exceptions.HTTPError(
response=mock_response
)

payload = {
'lms_user_id': 1234,
'content_key': 'course-v1:edX+edXPrivacy101+3T2020',
}

response = self.client.post(self.subsidy_access_policy_redeem_endpoint, payload)

response_json = self.load_json(response.content)

@mock.patch('enterprise_access.apps.subsidy_access_policy.models.get_and_cache_transactions_for_learner')
def test_redeem_policy_with_metadata(self, mock_transactions_cache_for_learner): # pylint: disable=unused-argument
"""
Expand Down
85 changes: 77 additions & 8 deletions enterprise_access/apps/api/v1/views/subsidy_access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
REASON_POLICY_NOT_ACTIVE,
REASON_POLICY_SPEND_LIMIT_REACHED,
MissingSubsidyAccessReasonUserMessages,
SubsidyRedemptionErrorCodes,
SubsidyRedemptionErrorReasons,
SubsidyFulfillmentErrorReasons,
TransactionStateChoices
)
from enterprise_access.apps.subsidy_access_policy.content_metadata_api import get_and_cache_content_metadata
Expand Down Expand Up @@ -485,11 +488,71 @@ def redeem(self, request, *args, **kwargs):
raise SubsidyAccessPolicyLockedException() from exc
except SubsidyAPIHTTPError as exc:
logger.exception(f'{exc} when creating transaction in subsidy API')
error_payload = exc.error_payload()
error_payload['detail'] = f"Subsidy Transaction API error: {error_payload['detail']}"
raise RedemptionRequestException(
detail=error_payload,
) from exc
try:
error_payload = self._get_subsidy_api_error_reason(policy, exc)
except Exception: # pylint: disable=broad-except
error_payload = exc.error_payload()
error_payload['detail'] = f"Subsidy Transaction API error: {error_payload['detail']}"

raise RedemptionRequestException(detail=error_payload) from exc

def _get_subsidy_api_error_reason(self, policy, exc_object):
"""
Helper to build error response payload on Subsidy API errors.
"""
subsidy_error_detail = exc_object.error_payload().get('detail')
subsidy_error_code = exc_object.error_payload().get('code')

metadata = {
'enterprise_administrators': self._get_enterprise_admin_users(policy.enterprise_customer_uuid),
}

# We currently only have enough data to say more specific things
# about fulfillment errors during subsidy API redemption.
if subsidy_error_code == SubsidyRedemptionErrorCodes.FULFILLMENT_ERROR:
return self._get_subsidy_fulfillment_error_reason(subsidy_error_detail, metadata)

default_reason = SubsidyRedemptionErrorReasons.DEFAULT_REASON
return {
'reason': default_reason,
'user_message': SubsidyRedemptionErrorReasons.USER_MESSAGES_BY_REASON[default_reason],
'metadata': metadata,
}

def _get_subsidy_fulfillment_error_reason(self, subsidy_error_detail, metadata):
"""
Helper to return a reason, user_message, and metadata
for the given subsidy_error_detail.
"""
reason = SubsidyFulfillmentErrorReasons.DEFAULT_REASON
metadata['subsidy_error_detail'] = subsidy_error_detail

if subsidy_error_detail:
message_string = self._get_subsidy_fulfillment_error_message(subsidy_error_detail)
if cause_of_message := SubsidyFulfillmentErrorReasons.get_cause_from_error_message(message_string):
reason = cause_of_message

return {
"reason": reason,
"user_message": SubsidyFulfillmentErrorReasons.USER_MESSAGES_BY_REASON.get(reason),
"metadata": metadata,
}

def _get_subsidy_fulfillment_error_message(self, subsidy_error_detail):
"""
``subsidy_error_detail`` is either a string describing an error message,
a dict with a "message" key describing an error message, or a list of such
dicts. This helper method widdles any of those things down into a single
error message string.
"""
if isinstance(subsidy_error_detail, str):
return subsidy_error_detail

subsidy_message_dict = subsidy_error_detail
if isinstance(subsidy_error_detail, list):
subsidy_message_dict = subsidy_error_detail[0]

return subsidy_message_dict.get('message')

def get_existing_redemptions(self, policies, lms_user_id):
"""
Expand Down Expand Up @@ -673,9 +736,7 @@ def _get_reasons_for_no_redeemable_policies(self, enterprise_customer_uuid, non_
for each non-redeemable policy.
"""
reasons = []
lms_client = LmsApiClient()
enterprise_customer_data = lms_client.get_enterprise_customer_data(enterprise_customer_uuid)
enterprise_admin_users = enterprise_customer_data.get('admin_users')
enterprise_admin_users = self._get_enterprise_admin_users(enterprise_customer_uuid)

for reason, policies in non_redeemable_policies.items():
reasons.append({
Expand All @@ -689,6 +750,14 @@ def _get_reasons_for_no_redeemable_policies(self, enterprise_customer_uuid, non_

return reasons

def _get_enterprise_admin_users(self, enterprise_customer_uuid):
"""
Helper to fetch admin users for the given customer uuid.
"""
lms_client = LmsApiClient()
enterprise_customer_data = lms_client.get_enterprise_customer_data(enterprise_customer_uuid)
return enterprise_customer_data.get('admin_users')

def _get_list_price(self, enterprise_customer_uuid, content_key):
"""
Determine the price for content for display purposes only.
Expand Down
54 changes: 54 additions & 0 deletions enterprise_access/apps/subsidy_access_policy/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" Constants for the subsidy_access_policy app. """
import re


class AccessMethods:
Expand Down Expand Up @@ -99,3 +100,56 @@ class MissingSubsidyAccessReasonUserMessages:
REASON_LEARNER_MAX_SPEND_REACHED = "learner_max_spend_reached"
REASON_POLICY_SPEND_LIMIT_REACHED = "policy_spend_limit_reached"
REASON_LEARNER_MAX_ENROLLMENTS_REACHED = "learner_max_enrollments_reached"


class SubsidyRedemptionErrorCodes:
"""
Collection of error ``code`` values that the subsidy API's
redeem endpoint might return in an error response payload.
"""
FULFILLMENT_ERROR = 'fulfillment_error'


class SubsidyRedemptionErrorReasons:
"""
Somewhat more generic collection of reasons that redemption may have
failed in ways that are *not* related to fulfillment.
"""
DEFAULT_REASON = 'default_subsidy_redemption_error'

USER_MESSAGES_BY_REASON = {
DEFAULT_REASON: "Something went wrong during subsidy redemption",
}


class SubsidyFulfillmentErrorReasons:
"""
Codifies standard reasons that fulfillment may have failed,
along with a mapping of those reasons to user-friendly display messages.
"""
DEFAULT_REASON = 'default_fulfillment_error'
DUPLICATE_FULFILLMENT = 'duplicate_fulfillment'

USER_MESSAGES_BY_REASON = {
DEFAULT_REASON: "Something went wrong during fulfillment",
DUPLICATE_FULFILLMENT: "A legacy fulfillment already exists for this content.",
}

CAUSES_REGEXP_BY_REASON = {
DUPLICATE_FULFILLMENT: re.compile(".*duplicate order.*"),
}

@staticmethod
def get_cause_from_error_message(message_string):
"""
Helper to find the cause of a given error message string
by matching against the regexs mapped above.
"""
if not message_string:
return None

for cause_of_message, regex in SubsidyRedemptionErrorReasons.CAUSES_REGEXP_BY_REASON.items():
if regex.match(message_string):
return cause_of_message

return None

0 comments on commit 08b78e4

Please sign in to comment.