Skip to content

Commit

Permalink
feat: Error if products in basket are already purchased (#3929)
Browse files Browse the repository at this point in the history
* feat: Error if products in basket are already purchased

* refactor: Add tests, Improve error message

* refactor: Update docstring

* test: Increase coverage
  • Loading branch information
moeez96 authored and christopappas committed Dec 4, 2023
1 parent 3e4ec38 commit b727b45
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 5 deletions.
39 changes: 39 additions & 0 deletions ecommerce/extensions/iap/api/v1/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import mock

from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.iap.api.v1.utils import products_in_basket_already_purchased
from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder
from ecommerce.extensions.test.factories import create_basket, create_order
from ecommerce.tests.testcases import TestCase


class TestProductsInBasketPurchased(TestCase):
""" Tests for products_in_basket_already_purchased method. """

def setUp(self):
super(TestProductsInBasketPurchased, self).setUp()
self.user = self.create_user()
self.client.login(username=self.user.username, password=self.password)

self.course = CourseFactory(partner=self.partner)
product = self.course.create_or_update_seat('verified', False, 50)
self.basket = create_basket(
owner=self.user, site=self.site, price='50.0', product_class=product.product_class
)
create_order(site=self.site, user=self.user, basket=self.basket)

def test_already_purchased(self):
"""
Test products in basket already purchased by user
"""
with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=True):
return_value = products_in_basket_already_purchased(self.user, self.basket, self.site)
self.assertTrue(return_value)

def test_not_purchased_yet(self):
"""
Test products in basket not yet purchased by user
"""
with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=False):
return_value = products_in_basket_already_purchased(self.user, self.basket, self.site)
self.assertFalse(return_value)
20 changes: 20 additions & 0 deletions ecommerce/extensions/iap/api/v1/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,19 @@ def test_post_order_exception(self, mock_handle_post_order):
self.assertEqual(response.status_code, expected_response_status_code)
self.assertEqual(response.content, expected_response_content)

def test_already_purchased_basket(self):
with mock.patch.object(GooglePlayValidator, 'validate') as fake_google_validation:
fake_google_validation.return_value = {
'resource': {
'orderId': 'orderId.android.test.purchased'
}
}
with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=True):
create_order(site=self.site, user=self.user, basket=self.basket)
response = self.client.post(self.path, data=self.post_data)
self.assertEqual(response.status_code, 406)
self.assertEqual(response.json().get('error'), ERROR_ALREADY_PURCHASED)


class TestMobileCheckoutView(TestCase):
""" Tests for MobileCheckoutView API view. """
Expand Down Expand Up @@ -533,6 +546,13 @@ def test_view_response(self):
self.assertIn(reverse('iap:iap-execute'), response_data['payment_page_url'])
self.assertEqual(response_data['payment_processor'], self.processor_name)

def test_already_purchased_basket(self):
with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=True):
create_order(site=self.site, user=self.user, basket=self.basket)
response = self.client.post(self.path, data=self.post_data)
self.assertEqual(response.status_code, 406)
self.assertEqual(response.json().get('error'), ERROR_ALREADY_PURCHASED)


class BaseRefundTests(RefundTestMixin, AccessTokenMixin, JwtMixin, TestCase):
MODEL_LOGGER_NAME = 'ecommerce.core.models'
Expand Down
19 changes: 19 additions & 0 deletions ecommerce/extensions/iap/api/v1/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@


from oscar.core.loading import get_model

from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder

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


def products_in_basket_already_purchased(user, basket, site):
"""
Check if products in a basket are already purchased by a user.
"""
products = Product.objects.filter(line__order__basket=basket)
for product in products:
if not product.is_enrollment_code_product and \
UserAlreadyPlacedOrder.user_already_placed_order(user=user, product=product, site=site):
return True
return False
23 changes: 18 additions & 5 deletions ecommerce/extensions/iap/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
)
from ecommerce.extensions.iap.api.v1.exceptions import RefundCompletionException
from ecommerce.extensions.iap.api.v1.serializers import MobileOrderSerializer
from ecommerce.extensions.iap.api.v1.utils import products_in_basket_already_purchased
from ecommerce.extensions.iap.models import IAPProcessorConfiguration
from ecommerce.extensions.iap.processors.android_iap import AndroidIAP
from ecommerce.extensions.iap.processors.ios_iap import IOSIAP
Expand Down Expand Up @@ -150,22 +151,24 @@ def payment_processor(self):

def _get_basket(self, request, basket_id):
"""
Retrieve a basket using a payment ID.
Retrieve a basket using a basket ID.
Arguments:
payment_id: payment_id received from PayPal.
basket_id: basket_id representing basket.
Returns:
It will return related basket or log exception and return None if
duplicate payment_id received or any other exception occurred.
It will return related basket or raise AlreadyPlacedOrderException
if products in basket have already been purchased.
"""
basket = request.user.baskets.get(id=basket_id)
basket.strategy = request.strategy

Applicator().apply(basket, basket.owner, self.request)
basket_add_organization_attribute(basket, self.request.GET)

if products_in_basket_already_purchased(request.user, basket, request.site):
raise AlreadyPlacedOrderException

return basket

# Disable atomicity for the view. Otherwise, we'd be unable to commit to the database
Expand All @@ -191,6 +194,8 @@ def post(self, request):
except ObjectDoesNotExist:
logger.exception(LOGGER_BASKET_NOT_FOUND, basket_id)
return JsonResponse({'error': ERROR_BASKET_NOT_FOUND.format(basket_id)}, status=400)
except AlreadyPlacedOrderException:
return JsonResponse({'error': _(ERROR_ALREADY_PURCHASED)}, status=406)
except: # pylint: disable=bare-except
error_message = ERROR_WHILE_OBTAINING_BASKET_FOR_USER.format(request.user.email)
logger.exception(error_message)
Expand Down Expand Up @@ -228,6 +233,14 @@ class MobileCheckoutView(APIView):
permission_classes = (IsAuthenticated,)

def post(self, request):
basket_id = request.data.get('basket_id')
try:
basket = request.user.baskets.get(id=basket_id)
except ObjectDoesNotExist:
return JsonResponse({'error': ERROR_BASKET_NOT_FOUND.format(basket_id)}, status=400)
if products_in_basket_already_purchased(request.user, basket, request.site):
return JsonResponse({'error': _(ERROR_ALREADY_PURCHASED)}, status=406)

response = CheckoutView.as_view()(request._request) # pylint: disable=W0212
if response.status_code != 200:
return JsonResponse({'error': response.content.decode()}, status=response.status_code)
Expand Down

0 comments on commit b727b45

Please sign in to comment.