def test_get_entitlement_voucher_with_invalid_entitlement_id(self): """ Verify that method "get_entitlement_voucher" logs exception if there is no coupon against the provided entitlement id in the enterprise learner API response. """ non_existing_coupon_id = 99 self.mock_access_token_response() self.mock_enterprise_learner_api( catalog_id=non_existing_coupon_id, entitlement_id=non_existing_coupon_id ) self.mock_enterprise_learner_entitlements_api(entitlement_id=non_existing_coupon_id) self.mock_catalog_contains_endpoint( discovery_api_url=self.site_configuration.discovery_api_url, catalog_id=non_existing_coupon_id, course_run_ids=[self.course.id] ) logger_name = 'ecommerce.enterprise.entitlements' with LogCapture(logger_name) as logger: entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(4) logger.check( ( logger_name, 'ERROR', 'There was an error getting coupon product with the entitlement id %s' % non_existing_coupon_id ) ) self.assertIsNone(entitlement_voucher)
def test_get_entitlement_voucher_with_invalid_entitlement_id(self): """ Verify that method "get_entitlement_voucher" logs exception if there is no coupon against the provided entitlement id in the enterprise learner API response. """ non_existing_coupon_id = 99 self.mock_enterprise_learner_api(catalog_id=non_existing_coupon_id, entitlement_id=non_existing_coupon_id) catalog_query = '*:*' self.mock_course_discovery_api_for_catalog_by_resource_id( catalog_id=non_existing_coupon_id, catalog_query=catalog_query) self.mock_dynamic_catalog_contains_api(query=catalog_query, course_run_ids=[self.course.id]) logger_name = 'ecommerce.enterprise.entitlements' with LogCapture(logger_name) as logger: entitlement_voucher = get_entitlement_voucher( self.request, self.course.products.first()) self._assert_num_requests(3) logger.check(( logger_name, 'ERROR', 'There was an error getting coupon product with the entitlement id %s' % non_existing_coupon_id)) self.assertIsNone(entitlement_voucher)
def test_get_entitlement_voucher_with_enterprise_feature_disabled(self): """ Verify that method "get_entitlement_voucher" doesn't call the enterprise service API and returns no voucher if the enterprise feature is disabled. """ self.mock_enterprise_learner_api() toggle_switch(settings.ENABLE_ENTERPRISE_ON_RUNTIME_SWITCH, False) entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(0) self.assertIsNone(entitlement_voucher)
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) voucher = Voucher.objects.get(code=code) if code else None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest( _('SKU [{sku}] does not exist.').format(sku=sku)) if voucher is None: # Find and apply the enterprise entitlement on the learner basket voucher = get_entitlement_voucher(request, product) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format( product=product.title) return HttpResponseBadRequest(msg) # If the product is not an Enrollment Code, we check to see if the user is already # enrolled to prevent double-enrollment and/or accidental coupon usage if product.get_product_class( ).name != ENROLLMENT_CODE_PRODUCT_CLASS_NAME: try: if request.user.is_user_already_enrolled(request, product): logger.warning( 'User [%s] attempted to repurchase the [%s] seat of course [%s]', request.user.username, mode_for_seat(product), product.attr.course_key) msg = _('You are already enrolled in {course}.').format( course=product.course.name) return HttpResponseBadRequest(msg) except (ConnectionError, SlumberBaseException, Timeout): msg = _( 'An error occurred while retrieving enrollment details. Please try again.' ) return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)
def test_get_entitlement_voucher_with_enterprise_feature_disabled(self): """ Verify that method "get_entitlement_voucher" doesn't call the enterprise service API and returns no voucher if the enterprise feature is disabled. """ self.mock_enterprise_learner_api() self.mock_enterprise_learner_entitlements_api() toggle_switch(settings.ENABLE_ENTERPRISE_ON_RUNTIME_SWITCH, False) entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(0) self.assertIsNone(entitlement_voucher)
def test_get_entitlement_voucher_with_enterprise_feature_enabled(self): """ Verify that method "get_entitlement_voucher" returns a voucher if the enterprise feature is enabled. """ coupon = self.create_coupon(catalog=self.catalog) expected_voucher = coupon.attr.coupon_vouchers.vouchers.first() self.mock_enterprise_learner_api(entitlement_id=coupon.id) entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(1) self.assertEqual(expected_voucher, entitlement_voucher)
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) voucher = Voucher.objects.get(code=code) if code else None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest(_('SKU [{sku}] does not exist.').format(sku=sku)) if voucher is None: # Find and apply the enterprise entitlement on the learner basket voucher = get_entitlement_voucher(request, product) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format(product=product.title) return HttpResponseBadRequest(msg) # If the product is not an Enrollment Code, we check to see if the user is already # enrolled to prevent double-enrollment and/or accidental coupon usage if product.get_product_class().name != ENROLLMENT_CODE_PRODUCT_CLASS_NAME: try: if request.user.is_user_already_enrolled(request, product): logger.warning( 'User [%s] attempted to repurchase the [%s] seat of course [%s]', request.user.username, mode_for_seat(product), product.attr.course_key ) msg = _('You are already enrolled in {course}.').format(course=product.course.name) return HttpResponseBadRequest(msg) except (ConnectionError, SlumberBaseException, Timeout): msg = _('An error occurred while retrieving enrollment details. Please try again.') return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled prepare_basket(request, product, voucher) return HttpResponseRedirect(reverse('basket:summary'), status=303)
def test_get_entitlement_voucher_with_enterprise_feature_enabled(self): """ Verify that method "get_entitlement_voucher" returns a voucher if the enterprise feature is enabled. """ coupon = self.create_coupon(catalog=self.catalog) expected_voucher = coupon.attr.coupon_vouchers.vouchers.first() catalog_query = '*:*' self.mock_enterprise_learner_api(entitlement_id=coupon.id) self.mock_enterprise_learner_entitlements_api(entitlement_id=coupon.id) self.mock_course_discovery_api_for_catalog_by_resource_id(catalog_query=catalog_query) self.mock_dynamic_catalog_contains_api(query=catalog_query, course_run_ids=[self.course.id]) entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(4) self.assertEqual(expected_voucher, entitlement_voucher)
def test_get_entitlement_voucher_with_enterprise_feature_enabled(self): """ Verify that method "get_entitlement_voucher" returns a voucher if the enterprise feature is enabled. """ self.mock_access_token_response() coupon = self.create_coupon(catalog=self.catalog) expected_voucher = coupon.attr.coupon_vouchers.vouchers.first() enterprise_catalog_id = 1 self.mock_enterprise_learner_api(entitlement_id=coupon.id) self.mock_enterprise_learner_entitlements_api(entitlement_id=coupon.id) self.mock_catalog_contains_endpoint( discovery_api_url=self.site_configuration.discovery_api_url, catalog_id=enterprise_catalog_id, course_run_ids=[self.course.id] ) entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(4) self.assertEqual(expected_voucher, entitlement_voucher)
def test_get_entitlement_voucher_with_invalid_entitlement_id(self): """ Verify that method "get_entitlement_voucher" logs exception if there is no coupon against the provided entitlement id in the enterprise learner API response. """ non_existing_coupon_id = 99 self.mock_enterprise_learner_api(entitlement_id=non_existing_coupon_id) logger_name = 'ecommerce.enterprise.entitlements' with LogCapture(logger_name) as logger: entitlement_voucher = get_entitlement_voucher(self.request, self.course.products.first()) self._assert_num_requests(1) logger.check( ( logger_name, 'ERROR', 'There was an error getting coupon product with the entitlement id %s' % non_existing_coupon_id ) ) self.assertIsNone(entitlement_voucher)
def get(self, request): # pylint: disable=too-many-statements """ Calculate basket totals given a list of sku's Create a temporary basket add the sku's and apply an optional voucher code. Then calculate the total price less discounts. If a voucher code is not provided apply a voucher in the Enterprise entitlements available to the user. Query Params: sku (string): A list of sku(s) to calculate code (string): Optional voucher code to apply to the basket. username (string): Optional username of a user for which to calculate the basket. Returns: JSON: { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } """ RequestCache.set(TEMPORARY_BASKET_CACHE_KEY, True) # TODO: LEARNER 5463 partner = get_partner_for_site(request) skus = request.GET.getlist('sku') if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) skus.sort() code = request.GET.get('code', None) try: voucher = Voucher.objects.get(code=code) if code else None except Voucher.DoesNotExist: voucher = None products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) # If there is only one product apply an Enterprise entitlement voucher if not voucher and len(products) == 1: voucher = get_entitlement_voucher(request, products[0]) basket_owner = request.user requested_username = request.GET.get('username', default='') is_anonymous = request.GET.get('is_anonymous', 'false').lower() == 'true' use_default_basket = is_anonymous # validate query parameters if requested_username and is_anonymous: return HttpResponseBadRequest( _('Provide username or is_anonymous query param, but not both') ) elif not requested_username and not is_anonymous: logger.warning( "Request to Basket Calculate must supply either username or is_anonymous query" " param. Requesting user=%s. Future versions of this API will treat this " "WARNING as an ERROR and raise an exception.", basket_owner.username) requested_username = request.user.username # If a username is passed in, validate that the user has staff access or is the same user. if requested_username: if basket_owner.username.lower() == requested_username.lower(): pass elif basket_owner.is_staff: try: basket_owner = User.objects.get( username=requested_username) except User.DoesNotExist: # This case represents a user who is logged in to marketing, but # doesn't yet have an account in ecommerce. These users have # never purchased before. use_default_basket = True else: return HttpResponseForbidden('Unauthorized user credentials') if basket_owner.username == self.MARKETING_USER and not use_default_basket: # For legacy requests that predate is_anonymous parameter, we will calculate # an anonymous basket if the calculated user is the marketing user. # TODO: LEARNER-5057: Remove this special case for the marketing user # once logs show no more requests with no parameters (see above). use_default_basket = True if use_default_basket: basket_owner = None cache_key = None if use_default_basket: # For an anonymous user we can directly get the cached price, because # there can't be any enrollments or entitlements. cache_key = get_cache_key(site_comain=request.site, resource_name='calculate', skus=skus) cached_response = TieredCache.get_cached_response(cache_key) if cached_response.is_hit: return Response(cached_response.value) if waffle.flag_is_active( request, "disable_calculate_temporary_basket_atomic_transaction"): response = self._calculate_temporary_basket( basket_owner, request, products, voucher, skus, code) else: response = self._calculate_temporary_basket_atomic( basket_owner, request, products, voucher, skus, code) if response and use_default_basket: TieredCache.set_all_tiers( cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT) return Response(response)
def get(self, request): partner = get_partner_for_site(request) sku = request.GET.get('sku', None) code = request.GET.get('code', None) consent_failed = request.GET.get(CONSENT_FAILED_PARAM, False) if not sku: return HttpResponseBadRequest(_('No SKU provided.')) voucher = Voucher.objects.get(code=code) if code else None try: product = StockRecord.objects.get(partner=partner, partner_sku=sku).product except StockRecord.DoesNotExist: return HttpResponseBadRequest( _('SKU [{sku}] does not exist.').format(sku=sku)) if not consent_failed and voucher is None: # Find and apply the enterprise entitlement on the learner basket. First, check two things: # 1. We don't already have an existing voucher parsed from a URL parameter # 2. The `consent_failed` URL parameter is falsey, or missing, meaning that we haven't already # attempted to apply an Enterprise voucher at least once, but the user rejected consent. Failing # to make that check would result in the user being repeatedly prompted to grant consent for the # same coupon they already declined consent on. voucher = get_entitlement_voucher(request, product) if voucher is not None: params = urlencode( OrderedDict([ ('code', voucher.code), ('sku', sku), # This view does not handle getting data sharing consent. However, the coupon redemption # view does. By adding the `failure_url` parameter, we're informing that view that, in the # event required consent for a coupon can't be collected, the user ought to be directed # back to this single-item basket view, with the `consent_failed` parameter applied so that # we know not to try to apply the enterprise coupon again. ( 'failure_url', request.build_absolute_uri( '{path}?{params}'.format( path=reverse('basket:single-item'), params=urlencode( OrderedDict([ (CONSENT_FAILED_PARAM, True), ('sku', sku), ])))), ), ])) return HttpResponseRedirect('{path}?{params}'.format( path=reverse('coupons:redeem'), params=params)) # If the product isn't available then there's no reason to continue with the basket addition purchase_info = request.strategy.fetch_for_product(product) if not purchase_info.availability.is_available_to_buy: msg = _('Product [{product}] not available to buy.').format( product=product.title) return HttpResponseBadRequest(msg) # At this point we're either adding an Enrollment Code product to the basket, # or the user is adding a Seat product for which they are not already enrolled try: prepare_basket(request, [product], voucher) except AlreadyPlacedOrderException: msg = _('You have already purchased {course} seat.').format( course=product.course.name) return render(request, 'edx/error.html', {'error': msg}) return HttpResponseRedirect(reverse('basket:summary'), status=303)
def get(self, request): """ Calculate basket totals given a list of sku's Create a temporary basket add the sku's and apply an optional voucher code. Then calculate the total price less discounts. If a voucher code is not provided apply a voucher in the Enterprise entitlements available to the user. Arguments: sku (string): A list of sku(s) to calculate code (string): Optional voucher code to apply to the basket. username (string): Optional username of a user for which to caclulate the basket. Returns: JSON: { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } """ partner = get_partner_for_site(request) skus = request.GET.getlist('sku') if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) code = request.GET.get('code', None) try: voucher = Voucher.objects.get(code=code) if code else None except Voucher.DoesNotExist: voucher = None products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) # If there is only one product apply an Enterprise entitlement voucher if not voucher and len(products) == 1: voucher = get_entitlement_voucher(request, products[0]) username = request.GET.get('username', default='') user = request.user # If a username is passed in, validate that the user has staff access or is the same user. if username: if user.is_staff or (user.username.lower() == username.lower()): try: user = User.objects.get(username=username) except User.DoesNotExist: logger.debug('Request username: [%s] does not exist', username) else: return HttpResponseForbidden('Unauthorized user credentials') # We wrap this in an atomic operation so we never commit this to the db. # This is to avoid merging this temporary basket with a real user basket. try: with transaction.atomic(): basket = Basket(owner=user, site=request.site) basket.strategy = Selector().strategy(user=user) for product in products: basket.add_product(product, 1) if voucher: basket.vouchers.add(voucher) # Calculate any discounts on the basket. Applicator().apply(basket, user=user, request=request) discounts = [] if basket.offer_discounts: discounts = basket.offer_discounts if basket.voucher_discounts: discounts.extend(basket.voucher_discounts) response = { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } raise api_exceptions.TemporaryBasketException except api_exceptions.TemporaryBasketException: pass except: # pylint: disable=bare-except logger.exception( 'Failed to calculate basket discount for SKUs [%s] and voucher [%s].', skus, code) raise return Response(response)
def get(self, request): """ Calculate basket totals given a list of sku's Create a temporary basket add the sku's and apply an optional voucher code. Then calculate the total price less discounts. If a voucher code is not provided apply a voucher in the Enterprise entitlements available to the user. Arguments: sku (string): A list of sku(s) to calculate code (string): Optional voucher code to apply to the basket. username (string): Optional username of a user for which to caclulate the basket. Returns: JSON: { 'total_incl_tax_excl_discounts': basket.total_incl_tax_excl_discounts, 'total_incl_tax': basket.total_incl_tax, 'currency': basket.currency } """ partner = get_partner_for_site(request) skus = request.GET.getlist('sku') if not skus: return HttpResponseBadRequest(_('No SKUs provided.')) code = request.GET.get('code', None) try: voucher = Voucher.objects.get(code=code) if code else None except Voucher.DoesNotExist: voucher = None products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) if not products: return HttpResponseBadRequest( _('Products with SKU(s) [{skus}] do not exist.').format( skus=', '.join(skus))) # If there is only one product apply an Enterprise entitlement voucher if not voucher and len(products) == 1: voucher = get_entitlement_voucher(request, products[0]) username = request.GET.get('username', default='') user = request.user # True if this request was made by the marketing user, and does not include a username # query param, and thus is calculating the non-logged in (anonymous) price. # Note: We need to verify separately that all calls without a username query param # can be treated in this same way. is_marketing_anonymous_request = False # If a username is passed in, validate that the user has staff access or is the same user. if username: if user.is_staff or (user.username.lower() == username.lower()): try: user = User.objects.get(username=username) except User.DoesNotExist: logger.debug('Request username: [%s] does not exist', username) else: return HttpResponseForbidden('Unauthorized user credentials') elif user.username == self.MARKETING_USER: is_marketing_anonymous_request = True cache_key = None # Since we know we can't have any enrollments or entitlements, we can directly get # the cached price. if is_marketing_anonymous_request: cache_key = get_cache_key(site_comain=request.site, resource_name='calculate', skus=skus) if waffle.flag_is_active( request, "use_cached_basket_calculate_for_marketing_user"): basket_calculate_results = cache.get(cache_key) if basket_calculate_results: return Response(basket_calculate_results) response = self._calculate_basket(user, request, products, voucher, skus, code) if response and is_marketing_anonymous_request: cache.set(cache_key, response, settings.ANONYMOUS_BASKET_CALCULATE_CACHE_TIMEOUT) return Response(response)