Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SONIC-704: Add CT discount availed check for outline tab #303

Merged
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",
NoyanAziz marked this conversation as resolved.
Show resolved Hide resolved
"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
syedsajjadkazmii marked this conversation as resolved.
Show resolved Hide resolved

for order in discounted_orders:
discount_code = order.discount_codes[0].discount_code.obj.code
syedsajjadkazmii marked this conversation as resolved.
Show resolved Hide resolved
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 protected]'
code = 'discount-code'

mock_orders = {
"total": 1,
"results": [
{
"discountCodes": [
{
"discountCode": {
"obj": {
"code": 'another-code'
}
}
}
]
}
]
}
NoyanAziz marked this conversation as resolved.
Show resolved Hide resolved

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 protected]'
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 = "[email protected]"
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 = '[email protected]'
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 @@
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})

Check failure on line 369 in commerce_coordinator/apps/lms/views.py

View workflow job for this annotation

GitHub Actions / tests (ubuntu-20.04, 3.12, django42)

Missing coverage

Missing coverage on line 369
Loading