def test_attribute_cookie_data_affiliate_cookie_lifecycle(self): """ Verify a basket is returned and referral captured if there is cookie info """ # If there is no cookie info, verify no referral is created. basket = BasketFactory(owner=self.request.user, site=self.request.site) attribute_cookie_data(basket, self.request) with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket=basket) # If there is cookie info, verify a referral is captured affiliate_id = 'test_affiliate' self.request.COOKIES['affiliate_id'] = affiliate_id attribute_cookie_data(basket, self.request) # test affiliate id from cookie saved in referral referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.affiliate_id, affiliate_id) # update cookie new_affiliate_id = 'new_affiliate' self.request.COOKIES['affiliate_id'] = new_affiliate_id attribute_cookie_data(basket, self.request) # test new affiliate id saved referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.affiliate_id, new_affiliate_id) # expire cookie del self.request.COOKIES['affiliate_id'] attribute_cookie_data(basket, self.request) # test referral record is deleted when no cookie set with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket_id=basket.id)
def create_new_basket(self, request, old_basket, *args, **kwargs): with transaction.atomic(): basket = None basket_id = None attribute_cookie_data(basket, request) requested_products = request.data.get('products') if requested_products: is_multi_product_basket = len(requested_products) > 1 basket = Basket.create_basket(request.site, request.user) basket.strategy = Selector().strategy(user=request.user) basket_id = basket.id for requested_product in requested_products: # Ensure the requested products exist sku = requested_product.get('sku') if sku: try: product = data_api.get_product(sku) except api_exceptions.ProductNotFoundError as error: return Response({"developer_message": "Can't Find Product."}) else: return Response({"developer_message": "Product not found."}) basket.add_product(product) # Call signal handler to notify listeners that something has been added to the basket basket_addition = get_class('basket.signals', 'basket_addition') basket_addition.send(sender=basket_addition, product=product, user=request.user, request=request, basket=basket, is_multi_product_basket=is_multi_product_basket) else: return Response({"developer_message": "No product provided."}) # Deletes old committed basket. if old_basket: old_basket.delete() basket.status = "Commited" basket.save() return Response({"basket":basket_id})
def test_attribute_cookie_data_multiple_cookies(self): """ Verify a basket is returned and referral captured. """ utm_source = 'test-source' utm_medium = 'test-medium' utm_campaign = 'test-campaign' utm_term = 'test-term' utm_content = 'test-content' utm_created_at = 1475590280823 utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } affiliate_id = 'affiliate' self.request.COOKIES[ self.site_configuration.utm_cookie_name] = json.dumps(utm_cookie) self.request.COOKIES['affiliate_id'] = affiliate_id basket = BasketFactory(owner=self.request.user, site=self.request.site) attribute_cookie_data(basket, self.request) # test affiliate id & UTM data from cookie saved in referral referral = Referral.objects.get(basket_id=basket.id) expected_created_at = datetime.datetime.fromtimestamp( int(utm_created_at) / float(1000), tz=pytz.UTC) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) self.assertEqual(referral.affiliate_id, affiliate_id) # expire 1 cookie del self.request.COOKIES[self.site_configuration.utm_cookie_name] attribute_cookie_data(basket, self.request) # test affiliate id still saved in referral but utm data removed referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, '') self.assertEqual(referral.utm_medium, '') self.assertEqual(referral.utm_campaign, '') self.assertEqual(referral.utm_term, '') self.assertEqual(referral.utm_content, '') self.assertIsNone(referral.utm_created_at) self.assertEqual(referral.affiliate_id, affiliate_id) # expire other cookie del self.request.COOKIES['affiliate_id'] attribute_cookie_data(basket, self.request) # test referral record is deleted when no cookies are set with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket_id=basket.id)
def test_attribute_cookie_data_multiple_cookies(self): """ Verify a basket is returned and referral captured. """ utm_source = 'test-source' utm_medium = 'test-medium' utm_campaign = 'test-campaign' utm_term = 'test-term' utm_content = 'test-content' utm_created_at = 1475590280823 utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } affiliate_id = 'affiliate' self.request.COOKIES['test.edx.utm'] = json.dumps(utm_cookie) self.request.COOKIES['affiliate_id'] = affiliate_id basket = BasketFactory(owner=self.request.user, site=self.request.site) attribute_cookie_data(basket, self.request) # test affiliate id & UTM data from cookie saved in referral referral = Referral.objects.get(basket_id=basket.id) expected_created_at = datetime.datetime.fromtimestamp(int(utm_created_at) / float(1000), tz=pytz.UTC) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) self.assertEqual(referral.affiliate_id, affiliate_id) # expire 1 cookie del self.request.COOKIES['test.edx.utm'] attribute_cookie_data(basket, self.request) # test affiliate id still saved in referral but utm data removed referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, '') self.assertEqual(referral.utm_medium, '') self.assertEqual(referral.utm_campaign, '') self.assertEqual(referral.utm_term, '') self.assertEqual(referral.utm_content, '') self.assertIsNone(referral.utm_created_at) self.assertEqual(referral.affiliate_id, affiliate_id) # expire other cookie del self.request.COOKIES['affiliate_id'] attribute_cookie_data(basket, self.request) # test referral record is deleted when no cookies are set with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket_id=basket.id)
def test_attribute_cookie_data_utm_cookie_lifecycle(self): """ Verify a basket is returned and referral captured. """ utm_source = 'test-source' utm_medium = 'test-medium' utm_campaign = 'test-campaign' utm_term = 'test-term' utm_content = 'test-content' utm_created_at = 1475590280823 expected_created_at = datetime.datetime.fromtimestamp( int(utm_created_at) / float(1000), tz=pytz.UTC) utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } self.request.COOKIES[ self.site_configuration.utm_cookie_name] = json.dumps(utm_cookie) basket = BasketFactory(owner=self.request.user, site=self.request.site) attribute_cookie_data(basket, self.request) # test utm data from cookie saved in referral referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) # update cookie utm_source = 'test-source-new' utm_medium = 'test-medium-new' utm_campaign = 'test-campaign-new' utm_term = 'test-term-new' utm_content = 'test-content-new' utm_created_at = 1470590000000 expected_created_at = datetime.datetime.fromtimestamp( int(utm_created_at) / float(1000), tz=pytz.UTC) new_utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } self.request.COOKIES[self.site_configuration. utm_cookie_name] = json.dumps(new_utm_cookie) attribute_cookie_data(basket, self.request) # test new utm data saved referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) # expire cookie del self.request.COOKIES[self.site_configuration.utm_cookie_name] attribute_cookie_data(basket, self.request) # test referral record is deleted when no cookie set with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket_id=basket.id)
def create(self, request, *args, **kwargs): """Add products to the authenticated user's basket. Expects an array of product objects, 'products', each containing a SKU, in the request body. The SKUs are used to populate the user's basket with the corresponding products. The caller indicates whether checkout should occur by providing a Boolean value in the request body, 'checkout'. If checkout operations are requested and the contents of the user's basket are free, an order is placed immediately. If checkout operations are requested but the contents of the user's basket are not free, pre-payment operations are performed instead of placing an order. The caller indicates which payment processor to use by providing a string in the request body, 'payment_processor_name'. Protected by JWT authentication. Consuming services (e.g., the LMS) must authenticate themselves by passing a JWT in the Authorization HTTP header, prepended with the string 'JWT '. The JWT payload should contain user details. At a minimum, these details must include a username; providing an email is recommended. Arguments: request (HttpRequest): With parameters 'products', 'checkout', and 'payment_processor_name' in the body. Returns: 200 if a basket was created successfully; the basket ID is included in the response body along with either an order number corresponding to the placed order (None if one wasn't placed) or payment information (None if payment isn't required). 400 if the client provided invalid data or attempted to add an unavailable product to their basket, with reason for the failure in JSON format. 401 if an unauthenticated request is denied permission to access the endpoint. 429 if the client has made requests at a rate exceeding that allowed by the configured rate limit. 500 if an error occurs when attempting to initiate checkout. Examples: Create a basket for the user with username 'Saul' as follows. Successful fulfillment requires that a user with username 'Saul' exists on the LMS, and that EDX_API_KEY be configured within both the LMS and the ecommerce service. >>> url = 'http://*****:*****@bettercallsaul.com'}, 'insecure-secret-key') >>> headers = { 'content-type': 'application/json', 'Authorization': 'JWT ' + token } If checkout is not desired: >>> data = {'products': [{'sku': 'SOME-SEAT'}, {'sku': 'SOME-OTHER-SEAT'}], 'checkout': False} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': None, 'payment_data': None } If the product with SKU 'FREE-SEAT' is free and checkout is desired: >>> data = {'products': [{'sku': 'FREE-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': {'number': 'OSCR-100007'}, 'payment_data': None } If the product with SKU 'PAID-SEAT' is not free and checkout is desired: >>> data = {'products': [{'sku': 'PAID-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': None, 'payment_data': { 'payment_processor_name': 'paypal', 'payment_form_data': {...}, 'payment_page_url': 'https://www.someexternallyhostedpaymentpage.com' } } """ # Explicitly delimit operations which will be rolled back if an exception occurs. # atomic() context managers restore atomicity at points where we are modifying data # (baskets, then orders) to ensure that we don't leave the system in a dirty state # in the event of an error. with transaction.atomic(): basket = Basket.create_basket(request.site, request.user) basket_id = basket.id attribute_cookie_data(basket, request) requested_products = request.data.get('products') if requested_products: is_multi_product_basket = True if len( requested_products) > 1 else False for requested_product in requested_products: # Ensure the requested products exist sku = requested_product.get('sku') if sku: try: product = data_api.get_product(sku) except api_exceptions.ProductNotFoundError as error: return self._report_bad_request( error.message, api_exceptions.PRODUCT_NOT_FOUND_USER_MESSAGE) else: return self._report_bad_request( api_exceptions.SKU_NOT_FOUND_DEVELOPER_MESSAGE, api_exceptions.SKU_NOT_FOUND_USER_MESSAGE) # Ensure the requested products are available for purchase before adding them to the basket availability = basket.strategy.fetch_for_product( product).availability if not availability.is_available_to_buy: return self._report_bad_request( api_exceptions. PRODUCT_UNAVAILABLE_DEVELOPER_MESSAGE.format( sku=sku, availability=availability.message), api_exceptions.PRODUCT_UNAVAILABLE_USER_MESSAGE) basket.add_product(product) logger.info('Added product with SKU [%s] to basket [%d]', sku, basket_id) # Call signal handler to notify listeners that something has been added to the basket basket_addition = get_class('basket.signals', 'basket_addition') basket_addition.send( sender=basket_addition, product=product, user=request.user, request=request, basket=basket, is_multi_product_basket=is_multi_product_basket) else: # If no products were included in the request, we cannot checkout. return self._report_bad_request( api_exceptions.PRODUCT_OBJECTS_MISSING_DEVELOPER_MESSAGE, api_exceptions.PRODUCT_OBJECTS_MISSING_USER_MESSAGE) if request.data.get('checkout') is True: # Begin the checkout process, if requested, with the requested payment processor. payment_processor_name = request.data.get('payment_processor_name') if payment_processor_name: try: payment_processor = get_processor_class_by_name( payment_processor_name) except payment_exceptions.ProcessorNotFoundError as error: return self._report_bad_request( error.message, payment_exceptions.PROCESSOR_NOT_FOUND_USER_MESSAGE) else: payment_processor = get_default_processor_class() try: response_data = self._checkout(basket, payment_processor(request.site), request) except Exception as ex: # pylint: disable=broad-except basket.delete() logger.exception( 'Failed to initiate checkout for Basket [%d]. The basket has been deleted.', basket_id) return Response({'developer_message': ex.message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: # Return a serialized basket, if checkout was not requested. response_data = self._generate_basic_response(basket) return Response(response_data, status=status.HTTP_200_OK)
def test_attribute_cookie_data_utm_cookie_lifecycle(self): """ Verify a basket is returned and referral captured. """ utm_source = 'test-source' utm_medium = 'test-medium' utm_campaign = 'test-campaign' utm_term = 'test-term' utm_content = 'test-content' utm_created_at = 1475590280823 expected_created_at = datetime.datetime.fromtimestamp(int(utm_created_at) / float(1000), tz=pytz.UTC) utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } self.request.COOKIES['test.edx.utm'] = json.dumps(utm_cookie) basket = BasketFactory(owner=self.request.user, site=self.request.site) attribute_cookie_data(basket, self.request) # test utm data from cookie saved in referral referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) # update cookie utm_source = 'test-source-new' utm_medium = 'test-medium-new' utm_campaign = 'test-campaign-new' utm_term = 'test-term-new' utm_content = 'test-content-new' utm_created_at = 1470590000000 expected_created_at = datetime.datetime.fromtimestamp(int(utm_created_at) / float(1000), tz=pytz.UTC) new_utm_cookie = { 'utm_source': utm_source, 'utm_medium': utm_medium, 'utm_campaign': utm_campaign, 'utm_term': utm_term, 'utm_content': utm_content, 'created_at': utm_created_at, } self.request.COOKIES['test.edx.utm'] = json.dumps(new_utm_cookie) attribute_cookie_data(basket, self.request) # test new utm data saved referral = Referral.objects.get(basket_id=basket.id) self.assertEqual(referral.utm_source, utm_source) self.assertEqual(referral.utm_medium, utm_medium) self.assertEqual(referral.utm_campaign, utm_campaign) self.assertEqual(referral.utm_term, utm_term) self.assertEqual(referral.utm_content, utm_content) self.assertEqual(referral.utm_created_at, expected_created_at) # expire cookie del self.request.COOKIES['test.edx.utm'] attribute_cookie_data(basket, self.request) # test referral record is deleted when no cookie set with self.assertRaises(Referral.DoesNotExist): Referral.objects.get(basket_id=basket.id)
def create(self, request, *args, **kwargs): """Add products to the authenticated user's basket. Expects an array of product objects, 'products', each containing a SKU, in the request body. The SKUs are used to populate the user's basket with the corresponding products. The caller indicates whether checkout should occur by providing a Boolean value in the request body, 'checkout'. If checkout operations are requested and the contents of the user's basket are free, an order is placed immediately. If checkout operations are requested but the contents of the user's basket are not free, pre-payment operations are performed instead of placing an order. The caller indicates which payment processor to use by providing a string in the request body, 'payment_processor_name'. Protected by JWT authentication. Consuming services (e.g., the LMS) must authenticate themselves by passing a JWT in the Authorization HTTP header, prepended with the string 'JWT '. The JWT payload should contain user details. At a minimum, these details must include a username; providing an email is recommended. Arguments: request (HttpRequest): With parameters 'products', 'checkout', and 'payment_processor_name' in the body. Returns: 200 if a basket was created successfully; the basket ID is included in the response body along with either an order number corresponding to the placed order (None if one wasn't placed) or payment information (None if payment isn't required). 400 if the client provided invalid data or attempted to add an unavailable product to their basket, with reason for the failure in JSON format. 401 if an unauthenticated request is denied permission to access the endpoint. 429 if the client has made requests at a rate exceeding that allowed by the configured rate limit. 500 if an error occurs when attempting to initiate checkout. Examples: Create a basket for the user with username 'Saul' as follows. Successful fulfillment requires that a user with username 'Saul' exists on the LMS, and that EDX_API_KEY be configured within both the LMS and the ecommerce service. >>> url = 'http://*****:*****@bettercallsaul.com'}, 'insecure-secret-key') >>> headers = { 'content-type': 'application/json', 'Authorization': 'JWT ' + token } If checkout is not desired: >>> data = {'products': [{'sku': 'SOME-SEAT'}, {'sku': 'SOME-OTHER-SEAT'}], 'checkout': False} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': None, 'payment_data': None } If the product with SKU 'FREE-SEAT' is free and checkout is desired: >>> data = {'products': [{'sku': 'FREE-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': {'number': 'OSCR-100007'}, 'payment_data': None } If the product with SKU 'PAID-SEAT' is not free and checkout is desired: >>> data = {'products': [{'sku': 'PAID-SEAT'}], 'checkout': True, 'payment_processor_name': 'paypal'} >>> response = requests.post(url, data=json.dumps(data), headers=headers) >>> response.json() { 'id': 7, 'order': None, 'payment_data': { 'payment_processor_name': 'paypal', 'payment_form_data': {...}, 'payment_page_url': 'https://www.someexternallyhostedpaymentpage.com' } } """ # Explicitly delimit operations which will be rolled back if an exception occurs. # atomic() context managers restore atomicity at points where we are modifying data # (baskets, then orders) to ensure that we don't leave the system in a dirty state # in the event of an error. with transaction.atomic(): basket = Basket.create_basket(request.site, request.user) basket_id = basket.id attribute_cookie_data(basket, request) requested_products = request.data.get('products') if requested_products: for requested_product in requested_products: # Ensure the requested products exist sku = requested_product.get('sku') if sku: try: product = data_api.get_product(sku) except api_exceptions.ProductNotFoundError as error: return self._report_bad_request( error.message, api_exceptions.PRODUCT_NOT_FOUND_USER_MESSAGE ) else: return self._report_bad_request( api_exceptions.SKU_NOT_FOUND_DEVELOPER_MESSAGE, api_exceptions.SKU_NOT_FOUND_USER_MESSAGE ) # Ensure the requested products are available for purchase before adding them to the basket availability = basket.strategy.fetch_for_product(product).availability if not availability.is_available_to_buy: return self._report_bad_request( api_exceptions.PRODUCT_UNAVAILABLE_DEVELOPER_MESSAGE.format( sku=sku, availability=availability.message ), api_exceptions.PRODUCT_UNAVAILABLE_USER_MESSAGE ) basket.add_product(product) logger.info('Added product with SKU [%s] to basket [%d]', sku, basket_id) # Call signal handler to notify listeners that something has been added to the basket basket_addition = get_class('basket.signals', 'basket_addition') basket_addition.send(sender=basket_addition, product=product, user=request.user, request=request, basket=basket) else: # If no products were included in the request, we cannot checkout. return self._report_bad_request( api_exceptions.PRODUCT_OBJECTS_MISSING_DEVELOPER_MESSAGE, api_exceptions.PRODUCT_OBJECTS_MISSING_USER_MESSAGE ) if request.data.get('checkout') is True: # Begin the checkout process, if requested, with the requested payment processor. payment_processor_name = request.data.get('payment_processor_name') if payment_processor_name: try: payment_processor = get_processor_class_by_name(payment_processor_name) except payment_exceptions.ProcessorNotFoundError as error: return self._report_bad_request( error.message, payment_exceptions.PROCESSOR_NOT_FOUND_USER_MESSAGE ) else: payment_processor = get_default_processor_class() try: response_data = self._checkout(basket, payment_processor(request.site), request) except Exception as ex: # pylint: disable=broad-except basket.delete() logger.exception('Failed to initiate checkout for Basket [%d]. The basket has been deleted.', basket_id) return Response({'developer_message': ex.message}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: # Return a serialized basket, if checkout was not requested. response_data = self._generate_basic_response(basket) return Response(response_data, status=status.HTTP_200_OK)