Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
feat: Embargo check for subscription Programs (#3960)
Browse files Browse the repository at this point in the history
  • Loading branch information
aht007 authored May 17, 2023
1 parent c623201 commit 3bae030
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 30 deletions.
70 changes: 55 additions & 15 deletions ecommerce/bff/subscriptions/tests/test_subscription_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework import status

from ecommerce.core.constants import COURSE_ENTITLEMENT_PRODUCT_CLASS_NAME
from ecommerce.core.models import SiteConfiguration
from ecommerce.coupons.tests.mixins import DiscoveryMockMixin
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.tests.factories import ProductFactory
Expand All @@ -25,8 +26,15 @@ def setUp(self):
super().setUp()
self.user = self.create_user(is_staff=True)
self.client.login(username=self.user.username, password=self.password)
self.ip_address = "mock_address"

def test_with_skus(self):
site_configuration = SiteConfiguration.objects.get(site=self.site)
site_configuration.enable_embargo_check = True
site_configuration.save()

@mock.patch('ecommerce.bff.subscriptions.views.embargo_check')
def test_with_skus(self, mock_embargo_check):
mock_embargo_check.return_value = True
product_class, _ = ProductClass.objects.get_or_create(name=COURSE_ENTITLEMENT_PRODUCT_CLASS_NAME)

product1 = ProductFactory(title="test product 1", product_class=product_class, stockrecords__partner=self.partner)
Expand All @@ -46,16 +54,18 @@ def test_with_skus(self):

url = reverse('bff:subscriptions:product-entitlement-info')

response = self.client.get(url, data=[('sku', product1.stockrecords.first().partner_sku),
('sku', product2.stockrecords.first().partner_sku)
])
response = self.client.post(url, data={'skus': [product1.stockrecords.first().partner_sku,
product2.stockrecords.first().partner_sku],
'user_ip_address': self.ip_address, 'username': self.user.username
})

self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [
expected_data = {'data': [
{'course_uuid': product1.attr.UUID, 'mode': product1.attr.certificate_type,
'sku': product1.stockrecords.first().partner_sku},
{'course_uuid': product2.attr.UUID, 'mode': product2.attr.certificate_type,
'sku': product2.stockrecords.first().partner_sku},
]
]}
self.assertCountEqual(json.loads(response.content.decode('utf-8')), expected_data)

@mock.patch('ecommerce.bff.subscriptions.views.logger.error')
Expand All @@ -75,29 +85,59 @@ def test_with_valid_and_invalid_products(self, mock_log):

url = reverse('bff:subscriptions:product-entitlement-info')

response = self.client.get(url, data=[('sku', product1.stockrecords.first().partner_sku),
('sku', product2.stockrecords.first().partner_sku)
])
response = self.client.post(url, data={'skus': [product1.stockrecords.first().partner_sku,
product2.stockrecords.first().partner_sku],
'user_ip_address': self.ip_address, 'username': self.user.username
})

mock_log.assert_called_once_with(f"B2C_SUBSCRIPTIONS: Product {product2}"
f"does not have a UUID attribute or mode is None")
f" does not have a UUID attribute or mode is None")
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [
expected_data = {'data': [
{'course_uuid': product1.attr.UUID, 'mode': product1.attr.certificate_type,
'sku': product1.stockrecords.first().partner_sku}
]
]}
self.assertCountEqual(json.loads(response.content.decode('utf-8')), expected_data)

def test_with_invalid_sku(self):
url = reverse('bff:subscriptions:product-entitlement-info')
response = self.client.get(url, data=[('sku', 1), ('sku', 2)])
response = self.client.post(url, data={'skus': ["blah", "blah-2"],
'user_ip_address': self.ip_address, 'username': self.user.username
})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
expected_data = {'error': 'Products with SKU(s) [1, 2] do not exist.'}
expected_data = {'error': 'Products with SKU(s) [blah, blah-2] do not exist.'}
self.assertCountEqual(json.loads(response.content.decode('utf-8')), expected_data)

def test_with_empty_sku(self):
url = reverse('bff:subscriptions:product-entitlement-info')
response = self.client.get(url)
response = self.client.post(url)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
expected_data = {'error': 'No SKUs provided.'}
self.assertCountEqual(json.loads(response.content.decode('utf-8')), expected_data)

@mock.patch('ecommerce.bff.subscriptions.views.embargo_check')
def test_embargo_failure(self, mock_embargo_check):
# In actual we don't expect Embargo to be False for any COURSE ENTITLEMENT product
# in its current Implementation. But we are mocking it to test the failure case.
# This will be fixed as a result of https://2u-internal.atlassian.net/browse/REV-3559

mock_embargo_check.return_value = False
product_class, _ = ProductClass.objects.get_or_create(name=COURSE_ENTITLEMENT_PRODUCT_CLASS_NAME)

product1 = ProductFactory(title="test product 1", product_class=product_class, stockrecords__partner=self.partner)
product1.attr.UUID = str(uuid.uuid4())
product1.attr.certificate_type = 'verified'
product1.attr.id_verification_required = False

product1.attr.save()
product1.refresh_from_db()

url = reverse('bff:subscriptions:product-entitlement-info')

response = self.client.post(url, data={'skus': [product1.stockrecords.first().partner_sku],
'user_ip_address': self.ip_address, 'username': self.user.username
})

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
expected_data = {'error': 'User blocked by embargo check', 'error_code': 'embargo_failed'}
self.assertCountEqual(json.loads(response.content.decode('utf-8')), expected_data)
38 changes: 25 additions & 13 deletions ecommerce/bff/subscriptions/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging

from django.http import HttpResponseBadRequest
from django.utils.html import escape
from django.contrib.auth import get_user_model
from oscar.core.loading import get_model
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
Expand All @@ -12,33 +11,49 @@
from ecommerce.extensions.api.exceptions import BadRequestException
from ecommerce.extensions.api.throttles import ServiceUserThrottle
from ecommerce.extensions.partner.shortcuts import get_partner_for_site
from ecommerce.extensions.payment.utils import embargo_check

logger = logging.getLogger(__name__)

Product = get_model('catalogue', 'Product')
User = get_user_model()


class ProductEntitlementInfoView(generics.GenericAPIView):

serializer_class = CourseEntitlementInfoSerializer
permission_classes = (IsAuthenticated, CanGetProductEntitlementInfo,)
permission_classes = (IsAuthenticated, CanGetProductEntitlementInfo)
throttle_classes = [ServiceUserThrottle]

def get(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
try:
skus = self._get_skus(self.request)
skus = request.POST.getlist('skus', [])
username = request.POST.get('username', None)
site = request.site
user_ip_address = request.POST.get('user_ip_address', None)

products = self._get_products_by_skus(skus)
available_products = self._get_available_products(products)
data = []
if request.site.siteconfiguration.enable_embargo_check:
if not embargo_check(username, site, available_products, user_ip_address):
logger.error(
'B2C_SUBSCRIPTIONS: User [%s] blocked by embargo, not continuing with the checkout process.',
username
)
return Response({'error': 'User blocked by embargo check',
'error_code': 'embargo_failed'},
status=status.HTTP_400_BAD_REQUEST)

for product in available_products:
mode = self._mode_for_product(product)
if hasattr(product.attr, 'UUID') and mode is not None:
data.append({'course_uuid': product.attr.UUID, 'mode': mode,
'sku': product.stockrecords.first().partner_sku})
else:
logger.error(f"B2C_SUBSCRIPTIONS: Product {product}"
"does not have a UUID attribute or mode is None")
return Response(data)
" does not have a UUID attribute or mode is None")
return Response({'data': data})
except BadRequestException as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)

Expand All @@ -56,6 +71,9 @@ def _get_available_products(self, products):
return available_products

def _get_products_by_skus(self, skus):
if not skus:
raise BadRequestException(('No SKUs provided.'))

partner = get_partner_for_site(self.request)
products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus)
if not products:
Expand All @@ -74,9 +92,3 @@ def _mode_for_product(self, product):
if mode == 'professional' and not getattr(product.attr, 'id_verification_required', False):
return 'no-id-professional'
return mode

def _get_skus(self, request):
skus = [escape(sku) for sku in request.GET.getlist('sku')]
if not skus:
raise BadRequestException(('No SKUs provided.'))
return skus
9 changes: 7 additions & 2 deletions ecommerce/extensions/payment/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import re
from urllib.parse import urljoin

from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from oscar.core.loading import get_model

Expand All @@ -12,6 +13,7 @@
Basket = get_model('basket', 'Basket')
BasketAttribute = get_model('basket', 'BasketAttribute')
BasketAttributeType = get_model('basket', 'BasketAttributeType')
User = get_user_model()


def get_basket_program_uuid(basket):
Expand Down Expand Up @@ -99,7 +101,7 @@ def clean_field_value(value):
return re.sub(r'[\^:"\']', '', value)


def embargo_check(user, site, products):
def embargo_check(user, site, products, ip=None):
""" Checks if the user has access to purchase products by calling the LMS embargo API.
Args:
Expand All @@ -109,8 +111,11 @@ def embargo_check(user, site, products):
Returns:
Bool
"""

courses = []
_, _, ip = parse_tracking_context(user, usage='embargo')

if not ip and isinstance(user, User):
_, _, ip = parse_tracking_context(user, usage='embargo')

for product in products:
# We only are checking Seats
Expand Down

0 comments on commit 3bae030

Please sign in to comment.