Esempio n. 1
0
    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)
Esempio n. 2
0
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
Esempio n. 3
0
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
Esempio n. 4
0
    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)
Esempio n. 5
0
 def setUp(self):
     super(EcommerceAPITests, self).setUp()
     self.url = reverse("commerce:orders")
     self.user = UserFactory()
     self.api = EcommerceAPI()
Esempio n. 6
0
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)
Esempio n. 7
0
 def setUp(self):
     super(EcommerceAPITests, self).setUp()
     self.url = reverse('commerce:baskets')
     self.user = UserFactory()
     self.api = EcommerceAPI()
Esempio n. 8
0
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)
Esempio n. 9
0
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)
Esempio n. 10
0
    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)
Esempio n. 11
0
    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)