Skip to content

Commit

Permalink
feat: added CT discount availed check for outline tab (#303)
Browse files Browse the repository at this point in the history
  • Loading branch information
NoyanAziz authored Jan 2, 2025
1 parent 41c5ca3 commit 1772b10
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 1 deletion.
36 changes: 36 additions & 0 deletions commerce_coordinator/apps/commercetools/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,3 +737,39 @@ def retire_customer_anonymize_fields(
f"error correlation id {err.correlation_id} and error/s: {err.errors}"
)
raise err

def is_first_time_discount_eligible(self, email: str, code: str) -> bool:
"""
Check if a user is eligible for a first time discount
Args:
email (str): Email of the user
code (str): First time discount code
Returns (bool): True if the user is eligible for a first time discount
"""
try:
discounted_orders = self.base_client.orders.query(
where=[
"customerEmail=:email",
"orderState=:orderState",
"discountCodes(discountCode is defined)"
],
predicate_var={'email': email, 'orderState': 'Complete'},
expand=["discountCodes[*].discountCode"]
)

if discounted_orders.total < 1:
return True

discounted_orders = discounted_orders.results

for order in discounted_orders:
discount_code = order.discount_codes[0].discount_code.obj.code
if discount_code == code:
return False

return True
except CommercetoolsError as err: # pragma no cover
# Logs & ignores version conflict errors due to duplicate Commercetools messages
handle_commercetools_error(err, f"Unable to check if user {email} is eligible for a "
f"first time discount", True)
return True
83 changes: 83 additions & 0 deletions commerce_coordinator/apps/commercetools/tests/test_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,89 @@ def test_update_customer_with_anonymized_fields_exception(self):

log_mock.assert_called_once_with(expected_message)

def test_is_first_time_discount_eligible_success(self):
base_url = self.client_set.get_base_url_from_client()
email = 'email@example.com'
code = 'discount-code'

mock_orders = {
"total": 1,
"results": [
{
"discountCodes": [
{
"discountCode": {
"obj": {
"code": 'another-code'
}
}
}
]
}
]
}

with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
mocker.get(
f"{base_url}orders",
json=mock_orders,
status_code=200
)

result = self.client_set.client.is_first_time_discount_eligible(email, code)
self.assertTrue(result)

def test_is_first_time_discount_not_eligible(self):
base_url = self.client_set.get_base_url_from_client()
email = 'email@example.com'
code = 'discount-code'

mock_orders = {
"total": 1,
"results": [
{
"discountCodes": [
{
"discountCode": {
"obj": {
"code": code
}
}
}
]
}
]
}

with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
mocker.get(
f"{base_url}orders",
json=mock_orders,
status_code=200
)

result = self.client_set.client.is_first_time_discount_eligible(email, code)
self.assertFalse(result)

def test_is_first_time_discount_eligible_invalid_email(self):
invalid_email = "invalid_email@example.com"
code = 'discount-code'
base_url = self.client_set.get_base_url_from_client()

mock_orders = {
"total": 0
}

with requests_mock.Mocker(real_http=True, case_sensitive=False) as mocker:
mocker.get(
f"{base_url}orders",
json=mock_orders,
status_code=200
)

result = self.client_set.client.is_first_time_discount_eligible(invalid_email, code)
self.assertTrue(result)


class PaginatedResultsTest(TestCase):
"""Tests for the simple logic in our Paginated Results Class"""
Expand Down
8 changes: 8 additions & 0 deletions commerce_coordinator/apps/lms/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,11 @@ class UserRetiredInputSerializer(CoordinatorSerializer):
Serializer for User Deactivation/Retirement input validation
"""
edx_lms_user_id = serializers.IntegerField(allow_null=False)


class FirstTimeDiscountInputSerializer(CoordinatorSerializer):
"""
Serializer for First Time Discount input validation
"""
email = serializers.EmailField(required=True)
code = serializers.CharField(required=True)
85 changes: 85 additions & 0 deletions commerce_coordinator/apps/lms/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,88 @@ def test_post_with_unexpected_exception_fails(self, mock_filter):
response = self.client.post(self.url, self.valid_payload, format='json')

self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)


@ddt.ddt
class FirstTimeDiscountEligibleViewTests(APITestCase):
"""
Tests for the FirstTimeDiscountEligibleView to check if a user is eligible for a first-time discount.
"""

test_user_username = 'test'
test_user_email = 'test@example.com'
test_user_password = 'secret'
test_discount = 'first_time_discount'

valid_payload = {
'email': test_user_email,
'code': test_discount,
}

invalid_payload = {
'email': None,
'code': 'any_discount',
}

url = reverse('lms:first_time_discount_eligible')

def setUp(self):
super().setUp()
self.user = User.objects.create_user(
self.test_user_username,
self.test_user_email,
self.test_user_password,
is_staff=True,
)

def tearDown(self):
super().tearDown()
self.client.logout()

def authenticate_user(self):
self.client.login(username=self.test_user_username, password=self.test_user_password)
self.client.force_authenticate(user=self.user)

@patch(
'commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient'
'.is_first_time_discount_eligible'
)
def test_get_with_valid_email_eligibility_true(self, mock_is_first_time_discount_eligible):
"""
Test case where the user is eligible for a first-time discount.
"""
self.authenticate_user()
mock_is_first_time_discount_eligible.return_value = True

response = self.client.post(self.url, self.valid_payload, format='json')

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {"is_eligible": True})
mock_is_first_time_discount_eligible.assert_called_once_with(self.test_user_email, self.test_discount)

@patch(
'commerce_coordinator.apps.commercetools.clients.CommercetoolsAPIClient'
'.is_first_time_discount_eligible'
)
def test_get_with_valid_email_eligibility_false(self, mock_is_first_time_discount_eligible):
"""
Test case where the user is not eligible for a first-time discount.
"""
self.authenticate_user()
mock_is_first_time_discount_eligible.return_value = False

response = self.client.post(self.url, self.valid_payload, format='json')

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {"is_eligible": False})
mock_is_first_time_discount_eligible.assert_called_once_with(self.test_user_email, self.test_discount)

def test_get_with_missing_email_fails(self):
"""
Test case where the email is not provided in the request query params.
"""
self.authenticate_user()

response = self.client.post(self.url, self.invalid_payload, format='json')

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
4 changes: 3 additions & 1 deletion commerce_coordinator/apps/lms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.urls import path

from commerce_coordinator.apps.lms.views import (
FirstTimeDiscountEligibleView,
OrderDetailsRedirectView,
PaymentPageRedirectView,
RefundView,
Expand All @@ -16,5 +17,6 @@
path('payment_page_redirect/', PaymentPageRedirectView.as_view(), name='payment_page_redirect'),
path('order_details_page_redirect/', OrderDetailsRedirectView.as_view(), name='order_details_page_redirect'),
path('refund/', RefundView.as_view(), name='refund'),
path('user_retirement/', RetirementView.as_view(), name='user_retirement')
path('user_retirement/', RetirementView.as_view(), name='user_retirement'),
path('first-time-discount-eligible/', FirstTimeDiscountEligibleView.as_view(), name='first_time_discount_eligible'),
]
33 changes: 33 additions & 0 deletions commerce_coordinator/apps/lms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@
import logging
from urllib.parse import urlencode, urljoin

from commercetools import CommercetoolsError
from django.conf import settings
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from edx_rest_framework_extensions.permissions import LoginRedirectIfUnauthenticated
from openedx_filters.exceptions import OpenEdxFilterException
from requests import HTTPError
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.status import HTTP_200_OK, HTTP_303_SEE_OTHER, HTTP_400_BAD_REQUEST, HTTP_500_INTERNAL_SERVER_ERROR
from rest_framework.throttling import UserRateThrottle
from rest_framework.views import APIView

from commerce_coordinator.apps.commercetools.clients import CommercetoolsAPIClient
from commerce_coordinator.apps.core.constants import HttpHeadersNames, MediaTypes
from commerce_coordinator.apps.lms.filters import (
OrderRefundRequested,
Expand All @@ -23,6 +26,7 @@
)
from commerce_coordinator.apps.lms.serializers import (
CourseRefundInputSerializer,
FirstTimeDiscountInputSerializer,
UserRetiredInputSerializer,
enrollment_attribute_key
)
Expand Down Expand Up @@ -334,3 +338,32 @@ def post(self, request) -> Response:
logger.exception(f"[RefundView] Exception raised in {self.post.__name__} with error {repr(e)}")
return Response('Exception occurred while retiring Commercetools customer',
status=HTTP_500_INTERNAL_SERVER_ERROR)


class FirstTimeDiscountEligibleView(APIView):
"""View to check if a user is eligible for a first time discount"""
permission_classes = [IsAdminUser]
throttle_classes = (UserRateThrottle,)

def post(self, request):
"""Return True if user is eligible for a first time discount."""
validator = FirstTimeDiscountInputSerializer(data=request.data)
validator.is_valid(raise_exception=True)

email = validator.validated_data['email']
code = validator.validated_data['code']

try:
ct_api_client = CommercetoolsAPIClient()
is_eligible = ct_api_client.is_first_time_discount_eligible(email, code)

output = {
'is_eligible': is_eligible
}
return Response(output)
except CommercetoolsError as err: # pragma no cover
logger.exception(f"[FirstTimeDiscountEligibleView] Commercetools Error: {err}, {err.errors}")
except HTTPError as err: # pragma no cover
logger.exception(f"[FirstTimeDiscountEligibleView] HTTP Error: {err}")

return Response({'is_eligible': True})

0 comments on commit 1772b10

Please sign in to comment.