def assert_successful_basket_creation(self, skus=None, checkout=None, payment_processor_name=None, requires_payment=False): """Verify that basket creation succeeded.""" # Ideally, we'd use Oscar's ShippingEventTypeFactory here, but it's not exposed/public. ShippingEventType.objects.get_or_create(name=SHIPPING_EVENT_NAME) with patch('ecommerce.extensions.analytics.utils.audit_log' ) as mock_audit_log: response = self.create_basket( skus=skus, checkout=checkout, payment_processor_name=payment_processor_name) self.assertEqual(response.status_code, 200) basket = Basket.objects.get() basket.strategy = Selector().strategy(user=self.user) self.assertEqual(response.data['id'], basket.id) if checkout: self.assertTrue( mock_audit_log.called_with('basket_frozen', amount=basket.total_excl_tax, basket_id=basket.id, currency=basket.currency, user_id=basket.owner.id)) if requires_payment: self.assertIsNone(response.data['order']) self.assertIsNotNone(response.data['payment_data'] ['payment_processor_name']) self.assertIsNotNone( response.data['payment_data']['payment_form_data']) if payment_processor_name is None: processor_class = get_default_processor_class() else: processor_class = get_processor_class_by_name( payment_processor_name) assert (response.data['payment_data']['payment_page_url'] == processor_class( Site.objects.all()[0]).client_side_payment_url) else: self.assertEqual(response.data['order']['number'], Order.objects.get().number) self.assertIsNone(response.data['payment_data']) else: self.assertIsNone(response.data['order']) self.assertIsNone(response.data['payment_data'])
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 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 requested_products = request.data.get(AC.KEYS.PRODUCTS) if requested_products: for requested_product in requested_products: # Ensure the requested products exist sku = requested_product.get(AC.KEYS.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) 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(AC.KEYS.CHECKOUT) is True: # Begin the checkout process, if requested, with the requested payment processor. payment_processor_name = request.data.get(AC.KEYS.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()) 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 create(self, request, *args, **kwargs): """Add products to the authenticated user's basket. Expects a list 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: HTTP_200_OK 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). HTTP_400_BAD_REQUEST if the client provided invalid data or attempted to add an unavailable product to their basket, with reason for the failure in JSON format. HTTP_401_UNAUTHORIZED if an unauthenticated request is denied permission to access the endpoint. HTTP_429_TOO_MANY_REQUESTS if the client has made requests at a rate exceeding that allowed by the configured rate limit. 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) >>> json.loads(response.content) { u'id': 7, u'order': None, u'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) >>> json.loads(response.content) { u'id': 7, u'order': {u'number': u'OSCR-100007'}, u'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) >>> json.loads(response.content) { u'id': 7, u'order': None, u'payment_data': { u'payment_processor_name': u'paypal', u'payment_form_data': {...}, u'payment_page_url': u'https://www.someexternallyhostedpaymentpage.com' } } """ basket = data.get_basket(request.user) requested_products = request.data.get(AC.KEYS.PRODUCTS) if requested_products: for requested_product in requested_products: sku = requested_product.get(AC.KEYS.SKU) if sku: try: product = data.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 ) 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( u"Added product with SKU [%s] to basket [%d]", sku, basket.id, ) else: return self._report_bad_request( api_exceptions.PRODUCT_OBJECTS_MISSING_DEVELOPER_MESSAGE, api_exceptions.PRODUCT_OBJECTS_MISSING_USER_MESSAGE ) if request.data.get(AC.KEYS.CHECKOUT) is True: payment_processor_name = request.data.get(AC.KEYS.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() response_data = self._checkout(basket, payment_processor=payment_processor()) else: response_data = self._generate_basic_response(basket) return Response(response_data, status=status.HTTP_200_OK)
def test_get_default_processor_class(self): """ Verify the function returns the first processor class defined in settings. """ self.assertIs(helpers.get_default_processor_class(), DummyProcessor)