def test_constructor_url_strip(self): """ Verifies that the URL is stored with trailing slashes removed. """ url = 'http://example.com' api = EcommerceAPI(url, 'edx') self.assertEqual(api.url, url) api = EcommerceAPI(url + '/', 'edx') self.assertEqual(api.url, url)
def create_order_with_ecommerce_service(user, course_key, course_mode): # pylint: disable=invalid-name """ Create a new order using the E-Commerce API. """ try: api = EcommerceAPI() # Make an API call to create the order and retrieve the results _order_number, _order_status, data = api.create_order(user, course_mode.sku) # Pass the payment parameters directly from the API response. return HttpResponse(json.dumps(data['payment_parameters']), content_type='application/json') except ApiError: params = {'username': user.username, 'mode': course_mode.slug, 'course_id': unicode(course_key)} log.error('Failed to create order for %(username)s %(mode)s mode of %(course_id)s', params) raise
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument """ Attempt to create the order and enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS # redirects to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) if not honor_mode: msg = Messages.NO_HONOR_MODE.format(course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) elif not honor_mode.sku: # If there are no course modes with SKUs, enroll the user without contacting the external API. msg = Messages.NO_SKU_ENROLLED.format( enrollment_mode=CourseMode.HONOR, course_id=course_id, username=user.username) log.debug(msg) self._enroll(course_key, user) return DetailResponse(msg) # Setup the API and report any errors if settings are not valid. try: api = EcommerceAPI() except InvalidConfigurationError: self._enroll(course_key, user) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) log.debug(msg) return DetailResponse(msg) # Make the API call try: order_number, order_status, _body = api.create_order( user, honor_mode.sku) if order_status == OrderStatus.COMPLETE: msg = Messages.ORDER_COMPLETED.format( order_number=order_number) log.debug(msg) return DetailResponse(msg) else: # TODO Before this functionality is fully rolled-out, this branch should be updated to NOT enroll the # user. Enrollments must be initiated by the E-Commerce API only. self._enroll(course_key, user) msg = u'Order %(order_number)s was received with %(status)s status. Expected %(complete_status)s. ' \ u'User %(username)s was enrolled in %(course_id)s by LMS.' msg_kwargs = { 'order_number': order_number, 'status': order_status, 'complete_status': OrderStatus.COMPLETE, 'username': user.username, 'course_id': course_id, } log.error(msg, msg_kwargs) msg = Messages.ORDER_INCOMPLETE_ENROLLED.format( order_number=order_number) return DetailResponse(msg, status=HTTP_202_ACCEPTED) except ApiError as err: # The API will handle logging of the error. return InternalRequestErrorResponse(err.message)
def setUp(self): super(EcommerceAPITests, self).setUp() self.url = reverse("commerce:orders") self.user = UserFactory() self.api = EcommerceAPI()
class EcommerceAPITests(EcommerceApiTestMixin, TestCase): """ Tests for the E-Commerce API client. """ SKU = "1234" def setUp(self): super(EcommerceAPITests, self).setUp() self.url = reverse("commerce:orders") self.user = UserFactory() self.api = EcommerceAPI() def test_constructor_url_strip(self): """ Verifies that the URL is stored with trailing slashes removed. """ url = "http://example.com" api = EcommerceAPI(url, "edx") self.assertEqual(api.url, url) api = EcommerceAPI(url + "/", "edx") self.assertEqual(api.url, url) @override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None) def test_no_settings(self): """ If the settings ECOMMERCE_API_URL and ECOMMERCE_API_SIGNING_KEY are invalid, the constructor should raise a ValueError. """ self.assertRaises(InvalidConfigurationError, EcommerceAPI) @httpretty.activate def test_create_order(self): """ Verify the method makes a call to the E-Commerce API with the correct headers and data. """ self._mock_ecommerce_api() number, status, body = self.api.create_order(self.user, self.SKU) # Validate the request sent to the E-Commerce API endpoint. request = httpretty.last_request() self.assertValidOrderRequest(request, self.user, self.ECOMMERCE_API_SIGNING_KEY, self.SKU) # Validate the data returned by the method self.assertEqual(number, self.ORDER_NUMBER) self.assertEqual(status, OrderStatus.COMPLETE) self.assertEqual(body, self.ECOMMERCE_API_SUCCESSFUL_BODY) @httpretty.activate @data(400, 401, 405, 406, 429, 500, 503) def test_create_order_with_invalid_http_status(self, status): """ If the E-Commerce API returns a non-200 status, the method should raise an InvalidResponseError. """ self._mock_ecommerce_api(status=status, body=json.dumps({"user_message": "FAIL!"})) self.assertRaises(InvalidResponseError, self.api.create_order, self.user, self.SKU) @httpretty.activate def test_create_order_with_invalid_json(self): """ If the E-Commerce API returns un-parseable data, the method should raise an InvalidResponseError. """ self._mock_ecommerce_api(body="TOTALLY NOT JSON!") self.assertRaises(InvalidResponseError, self.api.create_order, self.user, self.SKU) @httpretty.activate def test_create_order_with_timeout(self): """ If the call to the E-Commerce API times out, the method should raise a TimeoutError. """ def request_callback(_request, _uri, _headers): """ Simulates API timeout """ raise Timeout self._mock_ecommerce_api(body=request_callback) self.assertRaises(TimeoutError, self.api.create_order, self.user, self.SKU)
def setUp(self): super(EcommerceAPITests, self).setUp() self.url = reverse('commerce:baskets') self.user = UserFactory() self.api = EcommerceAPI()
class EcommerceAPITests(EcommerceApiTestMixin, TestCase): """ Tests for the E-Commerce API client. """ SKU = '1234' def setUp(self): super(EcommerceAPITests, self).setUp() self.url = reverse('commerce:baskets') self.user = UserFactory() self.api = EcommerceAPI() def test_constructor_url_strip(self): """ Verifies that the URL is stored with trailing slashes removed. """ url = 'http://example.com' api = EcommerceAPI(url, 'edx') self.assertEqual(api.url, url) api = EcommerceAPI(url + '/', 'edx') self.assertEqual(api.url, url) @override_settings(ECOMMERCE_API_URL=None, ECOMMERCE_API_SIGNING_KEY=None) def test_no_settings(self): """ If the settings ECOMMERCE_API_URL and ECOMMERCE_API_SIGNING_KEY are invalid, the constructor should raise a ValueError. """ self.assertRaises(InvalidConfigurationError, EcommerceAPI) @httpretty.activate @data(True, False) def test_create_basket(self, is_payment_required): """ Verify the method makes a call to the E-Commerce API with the correct headers and data. """ self._mock_ecommerce_api(is_payment_required=is_payment_required) response_data = self.api.create_basket(self.user, self.SKU, self.PROCESSOR) # Validate the request sent to the E-Commerce API endpoint. request = httpretty.last_request() self.assertValidBasketRequest(request, self.user, self.ECOMMERCE_API_SIGNING_KEY, self.SKU, self.PROCESSOR) # Validate the data returned by the method self.assertEqual(response_data['id'], self.BASKET_ID) if is_payment_required: self.assertEqual(response_data['order'], None) self.assertEqual(response_data['payment_data'], self.PAYMENT_DATA) else: self.assertEqual(response_data['order'], {"number": self.ORDER_NUMBER}) self.assertEqual(response_data['payment_data'], None) @httpretty.activate @data(400, 401, 405, 406, 429, 500, 503) def test_create_basket_with_invalid_http_status(self, status): """ If the E-Commerce API returns a non-200 status, the method should raise an InvalidResponseError. """ self._mock_ecommerce_api(status=status, body=json.dumps({'user_message': 'FAIL!'})) self.assertRaises(InvalidResponseError, self.api.create_basket, self.user, self.SKU, self.PROCESSOR) @httpretty.activate def test_create_basket_with_invalid_json(self): """ If the E-Commerce API returns un-parseable data, the method should raise an InvalidResponseError. """ self._mock_ecommerce_api(body='TOTALLY NOT JSON!') self.assertRaises(InvalidResponseError, self.api.create_basket, self.user, self.SKU, self.PROCESSOR) @httpretty.activate def test_create_basket_with_timeout(self): """ If the call to the E-Commerce API times out, the method should raise a TimeoutError. """ def request_callback(_request, _uri, _headers): """ Simulates API timeout """ raise Timeout self._mock_ecommerce_api(body=request_callback) self.assertRaises(TimeoutError, self.api.create_basket, self.user, self.SKU, self.PROCESSOR)
def _get_external_order(request, order_number): """Get the order context from the external E-Commerce Service. Get information about an order. This function makes a request to the E-Commerce Service to see if there is order information that can be used to render a receipt for the user. Args: request (Request): The request for the the receipt. order_number (str) : The order number. Returns: dict: A serializable dictionary of the receipt page context based on an order returned from the E-Commerce Service. """ try: api = EcommerceAPI() order_number, order_status, order_data = api.get_order(request.user, order_number) billing = order_data.get('billing_address', {}) country = billing.get('country', {}) # In order to get the date this order was paid, we need to check for payment sources, and associated # transactions. payment_dates = [] for source in order_data.get('sources', []): for transaction in source.get('transactions', []): payment_dates.append(dateutil.parser.parse(transaction['date_created'])) payment_date = sorted(payment_dates, reverse=True).pop() order_info = { 'orderNum': order_number, 'currency': order_data['currency'], 'status': order_status, 'purchase_datetime': get_default_time_display(payment_date), 'total_cost': order_data['total_excl_tax'], 'billed_to': { 'first_name': billing.get('first_name', ''), 'last_name': billing.get('last_name', ''), 'street1': billing.get('line1', ''), 'street2': billing.get('line2', ''), 'city': billing.get('line4', ''), # 'line4' is the City, from the E-Commerce Service 'state': billing.get('state', ''), 'postal_code': billing.get('postcode', ''), 'country': country.get('display_name', ''), }, 'items': [ { 'quantity': item['quantity'], 'unit_cost': item['unit_price_excl_tax'], 'line_cost': item['line_price_excl_tax'], 'line_desc': item['description'] } for item in order_data['lines'] ] } return JsonResponse(order_info) except InvalidConfigurationError: msg = u"E-Commerce API not setup. Cannot request Order [{order_number}] for User [{user_id}] ".format( user_id=request.user.id, order_number=order_number ) log.debug(msg) return JsonResponse(status=500, object={'error_message': msg}) except ApiError as err: # The API will handle logging of the error. return InternalRequestErrorResponse(err.message)
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument """ Attempt to create the order and enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS # redirects to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) if not honor_mode: msg = Messages.NO_HONOR_MODE.format(course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) elif not honor_mode.sku: # If there are no course modes with SKUs, enroll the user without contacting the external API. msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id, username=user.username) log.debug(msg) self._enroll(course_key, user) return DetailResponse(msg) # Setup the API and report any errors if settings are not valid. try: api = EcommerceAPI() except InvalidConfigurationError: self._enroll(course_key, user) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) log.debug(msg) return DetailResponse(msg) # Make the API call try: order_number, order_status, _body = api.create_order(user, honor_mode.sku) if order_status == OrderStatus.COMPLETE: msg = Messages.ORDER_COMPLETED.format(order_number=order_number) log.debug(msg) return DetailResponse(msg) else: # TODO Before this functionality is fully rolled-out, this branch should be updated to NOT enroll the # user. Enrollments must be initiated by the E-Commerce API only. self._enroll(course_key, user) msg = u'Order %(order_number)s was received with %(status)s status. Expected %(complete_status)s. ' \ u'User %(username)s was enrolled in %(course_id)s by LMS.' msg_kwargs = { 'order_number': order_number, 'status': order_status, 'complete_status': OrderStatus.COMPLETE, 'username': user.username, 'course_id': course_id, } log.error(msg, msg_kwargs) msg = Messages.ORDER_INCOMPLETE_ENROLLED.format(order_number=order_number) return DetailResponse(msg, status=HTTP_202_ACCEPTED) except ApiError as err: # The API will handle logging of the error. return InternalRequestErrorResponse(err.message)
def post(self, request, *args, **kwargs): # pylint: disable=unused-argument """ Attempt to create the basket and enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) # Don't do anything if an enrollment already exists course_id = unicode(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS # redirects to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) if not honor_mode: msg = Messages.NO_HONOR_MODE.format(course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) elif not honor_mode.sku: # If there are no course modes with SKUs, enroll the user without contacting the external API. msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id, username=user.username) log.debug(msg) self._enroll(course_key, user) return DetailResponse(msg) # Setup the API and report any errors if settings are not valid. try: api = EcommerceAPI() except InvalidConfigurationError: self._enroll(course_key, user) msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key)) log.debug(msg) return DetailResponse(msg) # Make the API call try: response_data = api.create_basket( user, honor_mode.sku, payment_processor="cybersource", ) payment_data = response_data["payment_data"] if payment_data is not None: # it is time to start the payment flow. # NOTE this branch does not appear to be used at the moment. return JsonResponse(payment_data) elif response_data['order']: # the order was completed immediately because there was no charge. msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number']) log.debug(msg) return DetailResponse(msg) else: # Enroll in the honor mode directly as a failsafe. # This MUST be removed when this code handles paid modes. self._enroll(course_key, user) msg = u'Unexpected response from basket endpoint.' log.error( msg + u' Could not enroll user %(username)s in course %(course_id)s.', {'username': user.id, 'course_id': course_id}, ) raise InvalidResponseError(msg) except ApiError as err: # The API will handle logging of the error. return InternalRequestErrorResponse(err.message)