Ejemplo n.º 1
0
    def post(self, request):
        basket_id = request.data['basket_id']
        payment_processor = request.data['payment_processor']

        # Get the basket, and make sure it belongs to the current user.
        try:
            basket = request.user.baskets.get(id=basket_id)
        except ObjectDoesNotExist:
            return HttpResponseBadRequest('Basket [{}] not found.'.format(basket_id))

        # Freeze the basket so that it cannot be modified
        basket.strategy = request.strategy
        Applicator().apply(basket, request.user, request)
        basket.freeze()

        # Return the payment info
        try:
            payment_processor = get_processor_class_by_name(payment_processor)()
        except ProcessorNotFoundError:
            logger.exception('Failed to get payment processor [%s].', payment_processor)
            return HttpResponseBadRequest(
                'Payment processor [{}] not found.'.format(payment_processor)
            )

        parameters = payment_processor.get_transaction_parameters(basket, request=request)
        payment_page_url = parameters.pop('payment_page_url')

        data = {
            'payment_form_data': parameters,
            'payment_page_url': payment_page_url,
            'payment_processor': payment_processor.NAME,
        }

        serializer = CheckoutSerializer(data)
        return Response(serializer.data)
Ejemplo n.º 2
0
    def _issue_credit(self):
        """Issue a credit/refund to the purchaser via the payment processor used for the original order."""
        try:
            # NOTE: Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)(
                self.order.site)
            amount = self.total_credit_excl_tax

            refund_reference_number = processor.issue_credit(
                self.order.number, self.order.basket, source.reference, amount,
                self.currency)
            source.refund(amount, reference=refund_reference_number)
            event_type, __ = PaymentEventType.objects.get_or_create(
                name=PaymentEventTypeName.REFUNDED)
            PaymentEvent.objects.create(event_type=event_type,
                                        order=self.order,
                                        amount=amount,
                                        reference=refund_reference_number,
                                        processor_name=processor.NAME)

            audit_log('credit_issued',
                      amount=amount,
                      currency=self.currency,
                      processor_name=processor.NAME,
                      refund_id=self.id,
                      user_id=self.user.id)
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Ejemplo n.º 3
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # NOTE: Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)(self.order.site)
            amount = self.total_credit_excl_tax

            refund_reference_number = processor.issue_credit(self.order, source.reference, amount, self.currency)
            source.refund(amount, reference=refund_reference_number)
            event_type, __ = PaymentEventType.objects.get_or_create(name=PaymentEventTypeName.REFUNDED)
            PaymentEvent.objects.create(
                event_type=event_type,
                order=self.order,
                amount=amount,
                reference=refund_reference_number,
                processor_name=processor.NAME
            )

            audit_log(
                'credit_issued',
                amount=amount,
                currency=self.currency,
                processor_name=processor.NAME,
                refund_id=self.id,
                user_id=self.user.id
            )
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Ejemplo n.º 4
0
def _get_payment_processor(site, name):
    key = (
        site,
        name,
    )
    payment_processor = _payment_processors.get(key)

    if not payment_processor:
        payment_processor = get_processor_class_by_name(name)(site)
        _payment_processors[key] = payment_processor

    return payment_processor
Ejemplo n.º 5
0
    def _clean_payment_processors(self):
        """
        Validates payment_processors field value

        Raises:
            ValidationError: If `payment_processors` field contains invalid/unknown payment_processor names
        """
        value = self.payment_processors.strip()
        if not value:
            raise ValidationError(
                'Invalid payment processors field: must not consist only of whitespace characters'
            )

        processor_names = value.split(',')
        for name in processor_names:
            try:
                get_processor_class_by_name(name.strip())
            except ProcessorNotFoundError as exc:
                log.exception(
                    "Exception validating site configuration for site `%s` - payment processor %s could not be found",
                    self.site.id, name)
                raise ValidationError(str(exc))
Ejemplo n.º 6
0
    def _clean_payment_processors(self):
        """
        Validates payment_processors field value

        Raises:
            ValidationError: If `payment_processors` field contains invalid/unknown payment_processor names
        """
        value = self.payment_processors.strip()
        if not value:
            raise ValidationError('Invalid payment processors field: must not consist only of whitespace characters')

        processor_names = value.split(',')
        for name in processor_names:
            try:
                get_processor_class_by_name(name.strip())
            except ProcessorNotFoundError as exc:
                log.exception(
                    "Exception validating site configuration for site `%s` - payment processor %s could not be found",
                    self.site.id,
                    name
                )
                raise ValidationError(exc.message)
Ejemplo n.º 7
0
    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'])
Ejemplo n.º 8
0
    def post(self, request):
        basket_id = request.data['basket_id']
        payment_processor_name = request.data['payment_processor']

        logger.info('Checkout view called for basket [%s].', basket_id)

        request._request.POST = request._request.POST.copy()  # pylint: disable=protected-access
        request._request.POST['discount_jwt'] = request.data.get(
            'discount_jwt')  # pylint: disable=protected-access

        # Get the basket, and make sure it belongs to the current user.
        try:
            basket = request.user.baskets.get(id=basket_id)
        except ObjectDoesNotExist:
            return HttpResponseBadRequest(
                'Basket [{}] not found.'.format(basket_id))

        # Freeze the basket so that it cannot be modified
        basket.strategy = request.strategy
        Applicator().apply(basket, request.user, request)
        basket.freeze()

        # Return the payment info
        try:
            payment_processor = get_processor_class_by_name(
                payment_processor_name)(request.site)
        except ProcessorNotFoundError:
            logger.exception(
                'Failed to get payment processor [%s]. basket id: [%s]. price: [%s]',
                payment_processor_name, basket_id, basket.total_excl_tax)

            return HttpResponseBadRequest(
                'Payment processor [{}] not found.'.format(
                    payment_processor_name))

        parameters = payment_processor.get_transaction_parameters(
            basket, request=request)
        payment_page_url = parameters.pop('payment_page_url')

        data = {
            'payment_form_data': parameters,
            'payment_page_url': payment_page_url,
            'payment_processor': payment_processor.NAME,
        }

        serializer = CheckoutSerializer(data)
        return Response(serializer.data)
Ejemplo n.º 9
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # TODO Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)()
            processor.issue_credit(source, self.total_credit_excl_tax,
                                   self.currency)

            audit_log('credit_issued',
                      amount=self.total_credit_excl_tax,
                      currency=self.currency,
                      processor_name=processor.NAME,
                      refund_id=self.id,
                      user_id=self.user.id)
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Ejemplo n.º 10
0
    def _issue_credit(self):
        """Issue a credit to the purchaser via the payment processor used for the original order."""
        try:
            # TODO Update this if we ever support multiple payment sources for a single order.
            source = self.order.sources.first()
            processor = get_processor_class_by_name(source.source_type.name)()
            processor.issue_credit(source, self.total_credit_excl_tax, self.currency)

            audit_log(
                'credit_issued',
                amount=self.total_credit_excl_tax,
                currency=self.currency,
                processor_name=processor.NAME,
                refund_id=self.id,
                user_id=self.user.id
            )
        except AttributeError:
            # Order has no sources, resulting in an exception when trying to access `source_type`.
            # This occurs when attempting to refund free orders.
            logger.info("No payments to credit for Refund [%d]", self.id)
Ejemplo n.º 11
0
    def post(self, request):
        basket_id = request.data['basket_id']
        payment_processor = request.data['payment_processor']

        # Get the basket, and make sure it belongs to the current user.
        try:
            basket = request.user.baskets.get(id=basket_id)
        except ObjectDoesNotExist:
            return HttpResponseBadRequest(
                'Basket [{}] not found.'.format(basket_id))

        # Freeze the basket so that it cannot be modified
        basket.strategy = request.strategy
        Applicator().apply(basket, request.user, request)
        basket.freeze()

        # Return the payment info
        try:
            payment_processor = get_processor_class_by_name(
                payment_processor)()
        except ProcessorNotFoundError:
            logger.exception('Failed to get payment processor [%s].',
                             payment_processor)
            return HttpResponseBadRequest(
                'Payment processor [{}] not found.'.format(payment_processor))

        parameters = payment_processor.get_transaction_parameters(
            basket, request=request)
        payment_page_url = parameters.pop('payment_page_url')

        data = {
            'payment_form_data': parameters,
            'payment_page_url': payment_page_url,
            'payment_processor': payment_processor.NAME,
        }

        serializer = CheckoutSerializer(data)
        return Response(serializer.data)
Ejemplo n.º 12
0
    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)
Ejemplo n.º 13
0
    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)
Ejemplo n.º 14
0
    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)
Ejemplo n.º 15
0
 def test_get_processor_class_by_name(self, processor):
     """ Verify the function returns the appropriate processor class or raises an exception, if not found. """
     self.assertIs(helpers.get_processor_class_by_name(processor.NAME), processor)
Ejemplo n.º 16
0
 def test_get_processor_class_by_name(self, processor):
     """ Verify the function returns the appropriate processor class or raises an exception, if not found. """
     self.assertIs(helpers.get_processor_class_by_name(processor.NAME),
                   processor)