Ejemplo n.º 1
0
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()

        self.basket = create_basket(owner=factories.UserFactory(), site=self.site)
        self.basket.freeze()

        self.processor = Paypal(self.site)
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')
Ejemplo n.º 2
0
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()
        self.price = '100.0'
        self.user = self.create_user()
        self.seat_product_class, __ = ProductClass.objects.get_or_create(name=SEAT_PRODUCT_CLASS_NAME)
        self.basket = create_basket(
            owner=self.user, site=self.site, price=self.price, product_class=self.seat_product_class
        )
        self.basket.freeze()

        self.processor = Paypal(self.site)
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')
Ejemplo n.º 3
0
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()

        self.basket = factories.create_basket()
        self.basket.owner = factories.UserFactory()
        self.basket.freeze()

        self.processor = Paypal()
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')
Ejemplo n.º 4
0
class PaypalPaymentExecutionView(EdxOrderPlacementMixin, View):
    """Execute an approved PayPal payment and place an order for paid products as appropriate."""
    payment_processor = Paypal()

    def _get_basket(self, payment_id):
        """
        Retrieve a basket using a payment ID.

        Arguments:
            payment_id: payment_id received from PayPal.

        Returns:
            It will return related basket or log exception and return None if
            duplicate payment_id received or any other exception occurred.

        """
        try:
            basket = PaymentProcessorResponse.objects.get(
                processor_name=self.payment_processor.NAME,
                transaction_id=payment_id).basket
            basket.strategy = strategy.Default()
            return basket
        except MultipleObjectsReturned:
            logger.exception(
                u"Duplicate payment ID [%s] received from PayPal.", payment_id)
            return None
        except Exception:  # pylint: disable=broad-except
            logger.exception(
                u"Unexpected error during basket retrieval while executing PayPal payment."
            )
            return None

    @transaction.non_atomic_requests
    def get(self, request):
        """Handle an incoming user returned to us by PayPal after approving payment."""
        payment_id = request.GET.get('paymentId')
        payer_id = request.GET.get('PayerID')
        logger.info(u"Payment [%s] approved by payer [%s]", payment_id,
                    payer_id)

        paypal_response = request.GET.dict()
        basket = self._get_basket(payment_id)

        if not basket:
            return redirect(self.payment_processor.error_url)

        receipt_url = u'{}?orderNum={}'.format(
            self.payment_processor.receipt_url, basket.order_number)

        try:
            with transaction.atomic():
                try:
                    self.handle_payment(paypal_response, basket)
                except PaymentError:
                    return redirect(receipt_url)
        except:  # pylint: disable=bare-except
            logger.exception(
                'Attempts to handle payment for basket [%d] failed.',
                basket.id)
            return redirect(receipt_url)

        try:
            shipping_method = NoShippingRequired()
            shipping_charge = shipping_method.calculate(basket)
            order_total = OrderTotalCalculator().calculate(
                basket, shipping_charge)

            user = basket.owner
            # Given a basket, order number generation is idempotent. Although we've already
            # generated this order number once before, it's faster to generate it again
            # than to retrieve an invoice number from PayPal.
            order_number = basket.order_number

            self.handle_order_placement(order_number=order_number,
                                        user=user,
                                        basket=basket,
                                        shipping_address=None,
                                        shipping_method=shipping_method,
                                        shipping_charge=shipping_charge,
                                        billing_address=None,
                                        order_total=order_total)

            return redirect(receipt_url)
        except:  # pylint: disable=bare-except
            logger.exception(self.order_placement_failure_msg, basket.id)
            return redirect(receipt_url)
Ejemplo n.º 5
0
 def payment_processor(self):
     return Paypal(self.request.site)
Ejemplo n.º 6
0
class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin,
                                      TestCase):
    """Test handling of users redirected by PayPal after approving payment."""
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()

        self.basket = create_basket(owner=factories.UserFactory(),
                                    site=self.site)
        self.basket.freeze()

        self.processor = Paypal(self.site)
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')

    @responses.activate
    def _assert_execution_redirect(self, payer_info=None, url_redirect=None):
        """Verify redirection to Otto receipt page after attempted payment execution."""
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)

        creation_response = self.mock_payment_creation_response(self.basket,
                                                                find=True)
        execution_response = self.mock_payment_execution_response(
            self.basket, payer_info=payer_info)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            url_redirect or get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration),
            fetch_redirect_response=False)

        return creation_response, execution_response

    def _assert_order_placement_failure(self, error_message):
        """Verify that order placement fails gracefully."""
        logger_name = 'ecommerce.extensions.payment.views.paypal'
        with LogCapture(logger_name) as l:
            __, execution_response = self._assert_execution_redirect()

            # Verify that the payment execution response was recorded despite the error
            self.assert_processor_response_recorded(self.processor_name,
                                                    self.PAYMENT_ID,
                                                    execution_response,
                                                    basket=self.basket)

            l.check((
                logger_name, 'INFO',
                'Payment [{payment_id}] approved by payer [{payer_id}]'.format(
                    payment_id=self.PAYMENT_ID, payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR', error_message))

    @responses.activate
    def test_execution_redirect_to_lms(self):
        """
        Verify redirection to LMS receipt page after attempted payment execution if the Otto receipt page is disabled.
        """
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)
        self.mock_payment_execution_response(self.basket)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration),
            fetch_redirect_response=False)

    @ddt.data(
        None,  # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
        {
            "shipping_address": None
        },  # minimal data, which may be sent in some Paypal execution responses
    )
    def test_payment_execution(self, payer_info):
        """Verify that a user who has approved payment is redirected to the configured receipt page."""
        self._assert_execution_redirect(payer_info=payer_info)
        # even if an exception occurs during handling of the payment notification, we still redirect the
        # user to the receipt page.  Therefore in addition to checking that the response had the correct
        # redirection, we also need to check that the order was actually created.
        self.get_order(self.basket)

    def test_payment_error(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_payment',
                side_effect=PaymentError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as l:
                creation_response, __ = self._assert_execution_redirect(
                    url_redirect=self.processor.error_url)
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                l.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)), )

    def test_unanticipated_error_during_payment_handling(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails in an unanticipated manner.
        """
        with mock.patch.object(PaypalPaymentExecutionView,
                               'handle_payment',
                               side_effect=KeyError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as l:
                creation_response, __ = self._assert_execution_redirect()
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                l.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Attempts to handle payment for basket [{basket_id}] failed.'
                     .format(basket_id=self.basket.id)),
                )

    def test_unable_to_place_order(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when the payment
        is executed but an order cannot be placed.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=UnableToPlaceOrder) as fake_handle_order_placement:
            error_message = 'Payment was received, but an order for basket [{basket_id}] could not be placed.'.format(
                basket_id=self.basket.id, )
            self._assert_order_placement_failure(error_message)
            self.assertTrue(fake_handle_order_placement.called)

    def test_unanticipated_error_during_order_placement(self):
        """Verify that unanticipated errors during order placement are handled gracefully."""
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=KeyError) as fake_handle_order_placement:
            error_message = 'Payment was received, but an order for basket [{basket_id}] could not be placed.'.format(
                basket_id=self.basket.id, )
            self._assert_order_placement_failure(error_message)
            self.assertTrue(fake_handle_order_placement.called)

    @responses.activate
    def test_payment_error_with_duplicate_payment_id(self):
        """
        Verify that we fail gracefully when PayPal sends us the wrong payment ID,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        logger_name = 'ecommerce.extensions.payment.views.paypal'
        with LogCapture(logger_name) as l:
            self.mock_oauth2_response()

            # Create payment records with different baskets which will have same payment ID
            self.mock_payment_creation_response(self.basket)
            self.processor.get_transaction_parameters(self.basket,
                                                      request=self.request)

            dummy_basket = create_basket()
            self.mock_payment_creation_response(dummy_basket)
            self.processor.get_transaction_parameters(dummy_basket,
                                                      request=self.request)

            self._assert_error_page_redirect()
            l.check(
                (logger_name, 'INFO',
                 'Payment [{payment_id}] approved by payer [{payer_id}]'.
                 format(payment_id=self.PAYMENT_ID, payer_id=self.PAYER_ID)),
                (
                    logger_name,
                    'WARNING',
                    'Duplicate payment ID [{payment_id}] received from PayPal.'
                    .format(payment_id=self.PAYMENT_ID),
                ),
            )

    @responses.activate
    def test_payment_error_with_no_basket(self):
        """
        Verify that we fail gracefully when any Exception occurred in _get_basket() method,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        with mock.patch.object(PaymentProcessorResponse.objects,
                               'get',
                               side_effect=Exception):
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as l:
                self.mock_oauth2_response()
                self.mock_payment_creation_response(self.basket)
                self.processor.get_transaction_parameters(self.basket,
                                                          request=self.request)
                self._assert_error_page_redirect()

                l.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Unexpected error during basket retrieval while executing PayPal payment.'
                     ),
                )

    def _assert_error_page_redirect(self):
        """Verify redirection to the configured checkout error page after attempted failed payment execution."""
        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)

        self.assertRedirects(response,
                             self.processor.error_url,
                             fetch_redirect_response=False)
Ejemplo n.º 7
0
 def payment_processor(self):
     return Paypal()
Ejemplo n.º 8
0
class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin,
                                      TestCase):
    """Test handling of users redirected by PayPal after approving payment."""
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()
        self.price = '100.0'
        self.user = self.create_user()
        self.seat_product_class, __ = ProductClass.objects.get_or_create(
            name=SEAT_PRODUCT_CLASS_NAME)
        self.basket = create_basket(owner=self.user,
                                    site=self.site,
                                    price=self.price,
                                    product_class=self.seat_product_class)
        self.basket.freeze()

        self.processor = Paypal(self.site)
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')

    @responses.activate
    def _assert_execution_redirect(self, payer_info=None, url_redirect=None):
        """Verify redirection to Otto receipt page after attempted payment execution."""
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)

        creation_response = self.mock_payment_creation_response(self.basket,
                                                                find=True)
        execution_response = self.mock_payment_execution_response(
            self.basket, payer_info=payer_info)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            url_redirect or get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

        return creation_response, execution_response

    def _assert_order_placement_failure(self, basket_id):
        """Verify that order placement fails gracefully."""
        logger_name = 'ecommerce.extensions.checkout.mixins'
        error_message = \
            'Order Failure: Paypal payment was received, but an order for basket [{basket_id}] ' \
            'could not be placed.'.format(basket_id=basket_id)
        with LogCapture(logger_name) as logger:
            __, execution_response = self._assert_execution_redirect()

            # Verify that the payment execution response was recorded despite the error
            self.assert_processor_response_recorded(self.processor_name,
                                                    self.PAYMENT_ID,
                                                    execution_response,
                                                    basket=self.basket)

            logger.check((logger_name, 'ERROR', error_message))

    @responses.activate
    def test_execution_redirect_to_lms(self):
        """
        Verify redirection to LMS receipt page after attempted payment execution if the Otto receipt page is disabled.
        """
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)
        self.mock_payment_execution_response(self.basket)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

    @responses.activate
    def test_execution_for_bulk_purchase(self):
        """
        Verify redirection to LMS receipt page after attempted payment
        execution if the Otto receipt page is disabled for bulk purchase and
        also that the order is linked to the provided business client..
        """
        toggle_switch(ENROLLMENT_CODE_SWITCH, True)
        self.mock_oauth2_response()

        course = CourseFactory(partner=self.partner)
        course.create_or_update_seat('verified',
                                     True,
                                     50,
                                     create_enrollment_code=True)
        self.basket = create_basket(owner=UserFactory(), site=self.site)
        enrollment_code = Product.objects.get(
            product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
        factories.create_stockrecord(enrollment_code,
                                     num_in_stock=2,
                                     price_excl_tax='10.00')
        self.basket.add_product(enrollment_code, quantity=1)

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)
        self.mock_payment_execution_response(self.basket)
        self.mock_payment_creation_response(self.basket, find=True)

        # Manually add organization attribute on the basket for testing
        self.RETURN_DATA.update({'organization': 'Dummy Business Client'})
        self.RETURN_DATA.update({PURCHASER_BEHALF_ATTRIBUTE: 'False'})
        basket_add_organization_attribute(self.basket, self.RETURN_DATA)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

        # Now verify that a new business client has been created and current
        # order is now linked with that client through Invoice model.
        order = Order.objects.filter(basket=self.basket).first()
        business_client = BusinessClient.objects.get(
            name=self.RETURN_DATA['organization'])
        assert Invoice.objects.get(
            order=order).business_client == business_client

    @ddt.data(
        None,  # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
        {
            "shipping_address": None
        },  # minimal data, which may be sent in some Paypal execution responses
    )
    def test_payment_execution(self, payer_info):
        """Verify that a user who has approved payment is redirected to the configured receipt page."""
        self._assert_execution_redirect(payer_info=payer_info)
        # even if an exception occurs during handling of the payment notification, we still redirect the
        # user to the receipt page.  Therefore in addition to checking that the response had the correct
        # redirection, we also need to check that the order was actually created.
        self.get_order(self.basket)

    def test_payment_error(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_payment',
                side_effect=PaymentError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                creation_response, __ = self._assert_execution_redirect(
                    url_redirect=self.processor.error_url)
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)), )

    def test_unanticipated_error_during_payment_handling(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails in an unanticipated manner.
        """
        with mock.patch.object(PaypalPaymentExecutionView,
                               'handle_payment',
                               side_effect=KeyError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                creation_response, __ = self._assert_execution_redirect()
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Attempts to handle payment for basket [{basket_id}] failed.'
                     .format(basket_id=self.basket.id)),
                )

    def test_unable_to_place_order(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when the payment
        is executed but an order cannot be placed.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=UnableToPlaceOrder) as fake_handle_order_placement:
            self._assert_order_placement_failure(self.basket.id)
            self.assertTrue(fake_handle_order_placement.called)

    def test_unanticipated_error_during_order_placement(self):
        """Verify that unanticipated errors during order placement are handled gracefully."""
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=KeyError) as fake_handle_order_placement:
            self._assert_order_placement_failure(self.basket.id)
            self.assertTrue(fake_handle_order_placement.called)

    def test_duplicate_order_attempt_logging(self):
        """
        Verify that attempts at creation of a duplicate order are logged correctly
        """
        prior_order = create_order()
        dummy_view = PaypalPaymentExecutionView()
        self.request.site = self.site
        dummy_view.request = self.request

        with LogCapture(
                self.DUPLICATE_ORDER_LOGGER_NAME) as lc, self.assertRaises(
                    Exception):
            dummy_view.create_order(request=self.request,
                                    basket=prior_order.basket)
            lc.check((self.DUPLICATE_ORDER_LOGGER_NAME, 'ERROR',
                      self.get_duplicate_order_error_message(
                          payment_processor='Paypal', order=prior_order)), )

    @responses.activate
    def test_payment_error_with_duplicate_payment_id(self):
        """
        Verify that we fail gracefully when PayPal sends us the wrong payment ID,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        logger_name = 'ecommerce.extensions.payment.views.paypal'
        with LogCapture(logger_name) as logger:
            self.mock_oauth2_response()

            # Create payment records with different baskets which will have same payment ID
            self.mock_payment_creation_response(self.basket)
            self.processor.get_transaction_parameters(self.basket,
                                                      request=self.request)

            dummy_basket = create_basket()
            self.mock_payment_creation_response(dummy_basket)
            self.processor.get_transaction_parameters(dummy_basket,
                                                      request=self.request)

            self._assert_error_page_redirect()
            logger.check(
                (logger_name, 'INFO',
                 'Payment [{payment_id}] approved by payer [{payer_id}]'.
                 format(payment_id=self.PAYMENT_ID, payer_id=self.PAYER_ID)),
                (
                    logger_name,
                    'WARNING',
                    'Duplicate payment ID [{payment_id}] received from PayPal.'
                    .format(payment_id=self.PAYMENT_ID),
                ),
            )

    @responses.activate
    def test_payment_error_with_no_basket(self):
        """
        Verify that we fail gracefully when any Exception occurred in _get_basket() method,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        with mock.patch.object(PaymentProcessorResponse.objects,
                               'get',
                               side_effect=Exception):
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                self.mock_oauth2_response()
                self.mock_payment_creation_response(self.basket)
                self.processor.get_transaction_parameters(self.basket,
                                                          request=self.request)
                self._assert_error_page_redirect()

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Unexpected error during basket retrieval while executing PayPal payment.'
                     ),
                )

    def _assert_error_page_redirect(self):
        """Verify redirection to the configured checkout error page after attempted failed payment execution."""
        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)

        self.assertRedirects(response,
                             self.processor.error_url,
                             fetch_redirect_response=False)
Ejemplo n.º 9
0
class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin, TestCase):
    """Test handling of users redirected by PayPal after approving payment."""

    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()

        self.basket = factories.create_basket()
        self.basket.owner = factories.UserFactory()
        self.basket.freeze()

        self.processor = Paypal()
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')

    @httpretty.activate
    def _assert_execution_redirect(self, payer_info=None):
        """Verify redirection to the configured receipt page after attempted payment execution."""
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket, request=self.request)

        creation_response = self.mock_payment_creation_response(self.basket, find=True)
        execution_response = self.mock_payment_execution_response(self.basket, payer_info=payer_info)

        response = self.client.get(reverse('paypal_execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            u'{}?basket_id={}'.format(self.processor.receipt_url, self.basket.id),
            fetch_redirect_response=False
        )

        return creation_response, execution_response

    def _assert_order_placement_failure(self, error_message):
        """Verify that order placement fails gracefully."""
        logger_name = 'ecommerce.extensions.payment.views'
        with LogCapture(logger_name) as l:
            __, execution_response = self._assert_execution_redirect()

            # Verify that the payment execution response was recorded despite the error
            self.assert_processor_response_recorded(
                self.processor_name,
                self.PAYMENT_ID,
                execution_response,
                basket=self.basket
            )

            l.check(
                (
                    logger_name,
                    'INFO',
                    'Payment [{payment_id}] approved by payer [{payer_id}]'.format(
                        payment_id=self.PAYMENT_ID,
                        payer_id=self.PAYER_ID
                    )
                ),
                (logger_name, 'ERROR', error_message)
            )

    @ddt.data(
        None,  # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
        {"shipping_address": None},  # minimal data, which may be sent in some Paypal execution responses
    )
    def test_payment_execution(self, payer_info):
        """Verify that a user who has approved payment is redirected to the configured receipt page."""
        self._assert_execution_redirect(payer_info=payer_info)
        # even if an exception occurs during handling of the payment notification, we still redirect the
        # user to the receipt page.  Therefore in addition to checking that the response had the correct
        # redirection, we also need to check that the order was actually created.
        self.get_order(self.basket)

    def test_payment_error(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails.
        """
        with mock.patch.object(PaypalPaymentExecutionView, 'handle_payment',
                               side_effect=PaymentError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views'
            with LogCapture(logger_name) as l:
                creation_response, __ = self._assert_execution_redirect()
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(
                    self.processor_name,
                    self.PAYMENT_ID,
                    creation_response,
                    basket=self.basket
                )

                l.check(
                    (
                        logger_name,
                        'INFO',
                        'Payment [{payment_id}] approved by payer [{payer_id}]'.format(
                            payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID
                        )
                    ),
                )

    def test_unanticipated_error_during_payment_handling(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails in an unanticipated manner.
        """
        with mock.patch.object(PaypalPaymentExecutionView, 'handle_payment',
                               side_effect=KeyError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views'
            with LogCapture(logger_name) as l:
                creation_response, __ = self._assert_execution_redirect()
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(
                    self.processor_name,
                    self.PAYMENT_ID,
                    creation_response,
                    basket=self.basket
                )

                l.check(
                    (
                        logger_name,
                        'INFO',
                        'Payment [{payment_id}] approved by payer [{payer_id}]'.format(
                            payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID
                        )
                    ),
                    (
                        logger_name,
                        'ERROR',
                        'Attempts to handle payment for basket [{basket_id}] failed.'.format(basket_id=self.basket.id)
                    ),
                )

    def test_unable_to_place_order(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when the payment
        is executed but an order cannot be placed.
        """
        with mock.patch.object(PaypalPaymentExecutionView, 'handle_order_placement',
                               side_effect=UnableToPlaceOrder) as fake_handle_order_placement:
            error_message = 'Payment was executed, but an order was not created for basket [{basket_id}].'.format(
                basket_id=self.basket.id
            )
            self._assert_order_placement_failure(error_message)
            self.assertTrue(fake_handle_order_placement.called)

    def test_unanticipated_error_during_order_placement(self):
        """Verify that unanticipated errors during order placement are handled gracefully."""
        with mock.patch.object(PaypalPaymentExecutionView, 'handle_order_placement',
                               side_effect=KeyError) as fake_handle_order_placement:
            error_message = 'Payment was received, but attempts to create an order for '\
                            'basket [{basket_id}] failed.'.format(basket_id=self.basket.id)
            self._assert_order_placement_failure(error_message)
            self.assertTrue(fake_handle_order_placement.called)
Ejemplo n.º 10
0
class PaypalPaymentExecutionViewTests(PaypalMixin, PaymentEventsMixin,
                                      TestCase):
    """Test handling of users redirected by PayPal after approving payment."""
    def setUp(self):
        super(PaypalPaymentExecutionViewTests, self).setUp()
        self.price = '100.0'
        self.user = self.create_user()
        self.basket = create_basket(owner=self.user,
                                    site=self.site,
                                    price=self.price)
        self.basket.freeze()

        self.processor = Paypal(self.site)
        self.processor_name = self.processor.NAME

        # Dummy request from which an HTTP Host header can be extracted during
        # construction of absolute URLs
        self.request = RequestFactory().post('/')

    @responses.activate
    def _assert_execution_redirect(self, payer_info=None, url_redirect=None):
        """Verify redirection to Otto receipt page after attempted payment execution."""
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)

        creation_response = self.mock_payment_creation_response(self.basket,
                                                                find=True)
        execution_response = self.mock_payment_execution_response(
            self.basket, payer_info=payer_info)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            url_redirect or get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

        return creation_response, execution_response

    def _assert_order_placement_failure(self, basket_id):
        """Verify that order placement fails gracefully."""
        logger_name = 'ecommerce.extensions.checkout.mixins'
        error_message = \
            'Order Failure: Paypal payment was received, but an order for basket [{basket_id}] ' \
            'could not be placed.'.format(basket_id=basket_id)
        with LogCapture(logger_name) as logger:
            __, execution_response = self._assert_execution_redirect()

            # Verify that the payment execution response was recorded despite the error
            self.assert_processor_response_recorded(self.processor_name,
                                                    self.PAYMENT_ID,
                                                    execution_response,
                                                    basket=self.basket)

            logger.check((logger_name, 'ERROR', error_message))

    @responses.activate
    def test_execution_redirect_to_lms(self):
        """
        Verify redirection to LMS receipt page after attempted payment execution if the Otto receipt page is disabled.
        """
        self.mock_oauth2_response()

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)
        self.mock_payment_execution_response(self.basket)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

    @responses.activate
    def test_execution_for_bulk_purchase(self):
        """
        Verify redirection to LMS receipt page after attempted payment
        execution if the Otto receipt page is disabled for bulk purchase and
        also that the order is linked to the provided business client..
        """
        toggle_switch(ENROLLMENT_CODE_SWITCH, True)
        self.mock_oauth2_response()

        course = CourseFactory(partner=self.partner)
        course.create_or_update_seat('verified',
                                     True,
                                     50,
                                     create_enrollment_code=True)
        self.basket = create_basket(owner=UserFactory(), site=self.site)
        enrollment_code = Product.objects.get(
            product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME)
        factories.create_stockrecord(enrollment_code,
                                     num_in_stock=2,
                                     price_excl_tax='10.00')
        self.basket.add_product(enrollment_code, quantity=1)

        # Create a payment record the view can use to retrieve a basket
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)
        self.mock_payment_execution_response(self.basket)
        self.mock_payment_creation_response(self.basket, find=True)

        # Manually add organization attribute on the basket for testing
        self.RETURN_DATA.update({'organization': 'Dummy Business Client'})
        basket_add_organization_attribute(self.basket, self.RETURN_DATA)

        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)
        self.assertRedirects(
            response,
            get_receipt_page_url(
                order_number=self.basket.order_number,
                site_configuration=self.basket.site.siteconfiguration,
                disable_back_button=True,
            ),
            fetch_redirect_response=False)

        # Now verify that a new business client has been created and current
        # order is now linked with that client through Invoice model.
        order = Order.objects.filter(basket=self.basket).first()
        business_client = BusinessClient.objects.get(
            name=self.RETURN_DATA['organization'])
        assert Invoice.objects.get(
            order=order).business_client == business_client

    @ddt.data(
        None,  # falls back to PaypalMixin.PAYER_INFO, a fully-populated payer_info object
        {
            "shipping_address": None
        },  # minimal data, which may be sent in some Paypal execution responses
    )
    def test_payment_execution(self, payer_info):
        """Verify that a user who has approved payment is redirected to the configured receipt page."""
        self._assert_execution_redirect(payer_info=payer_info)
        # even if an exception occurs during handling of the payment notification, we still redirect the
        # user to the receipt page.  Therefore in addition to checking that the response had the correct
        # redirection, we also need to check that the order was actually created.
        self.get_order(self.basket)

    def test_payment_error(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_payment',
                side_effect=PaymentError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                creation_response, __ = self._assert_execution_redirect(
                    url_redirect=self.processor.error_url)
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)), )

    def test_unanticipated_error_during_payment_handling(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when payment
        execution fails in an unanticipated manner.
        """
        with mock.patch.object(PaypalPaymentExecutionView,
                               'handle_payment',
                               side_effect=KeyError) as fake_handle_payment:
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                creation_response, __ = self._assert_execution_redirect()
                self.assertTrue(fake_handle_payment.called)

                # Verify that the payment creation response was recorded despite the error
                self.assert_processor_response_recorded(self.processor_name,
                                                        self.PAYMENT_ID,
                                                        creation_response,
                                                        basket=self.basket)

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Attempts to handle payment for basket [{basket_id}] failed.'
                     .format(basket_id=self.basket.id)),
                )

    def test_unable_to_place_order(self):
        """
        Verify that a user who has approved payment is redirected to the configured receipt page when the payment
        is executed but an order cannot be placed.
        """
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=UnableToPlaceOrder) as fake_handle_order_placement:
            self._assert_order_placement_failure(self.basket.id)
            self.assertTrue(fake_handle_order_placement.called)

    def test_unanticipated_error_during_order_placement(self):
        """Verify that unanticipated errors during order placement are handled gracefully."""
        with mock.patch.object(
                PaypalPaymentExecutionView,
                'handle_order_placement',
                side_effect=KeyError) as fake_handle_order_placement:
            self._assert_order_placement_failure(self.basket.id)
            self.assertTrue(fake_handle_order_placement.called)

    def test_duplicate_order_attempt_logging(self):
        """
        Verify that attempts at creation of a duplicate order are logged correctly
        """
        prior_order = create_order()
        dummy_view = PaypalPaymentExecutionView()
        self.request.site = self.site
        dummy_view.request = self.request

        with LogCapture(self.DUPLICATE_ORDER_LOGGER_NAME) as lc:
            dummy_view.call_handle_order_placement(prior_order.basket,
                                                   self.request)
            lc.check((self.DUPLICATE_ORDER_LOGGER_NAME, 'ERROR',
                      self.get_duplicate_order_error_message(
                          payment_processor='Paypal', order=prior_order)), )

    @responses.activate
    def test_payment_error_with_duplicate_payment_id(self):
        """
        Verify that we fail gracefully when PayPal sends us the wrong payment ID,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        logger_name = 'ecommerce.extensions.payment.views.paypal'
        with LogCapture(logger_name) as logger:
            self.mock_oauth2_response()

            # Create payment records with different baskets which will have same payment ID
            self.mock_payment_creation_response(self.basket)
            self.processor.get_transaction_parameters(self.basket,
                                                      request=self.request)

            dummy_basket = create_basket()
            self.mock_payment_creation_response(dummy_basket)
            self.processor.get_transaction_parameters(dummy_basket,
                                                      request=self.request)

            self._assert_error_page_redirect()
            logger.check(
                (logger_name, 'INFO',
                 'Payment [{payment_id}] approved by payer [{payer_id}]'.
                 format(payment_id=self.PAYMENT_ID, payer_id=self.PAYER_ID)),
                (
                    logger_name,
                    'WARNING',
                    'Duplicate payment ID [{payment_id}] received from PayPal.'
                    .format(payment_id=self.PAYMENT_ID),
                ),
            )

    @responses.activate
    def test_payment_error_with_no_basket(self):
        """
        Verify that we fail gracefully when any Exception occurred in _get_basket() method,
        logging the exception and redirecting the user to an LMS checkout error page.
        """
        with mock.patch.object(PaymentProcessorResponse.objects,
                               'get',
                               side_effect=Exception):
            logger_name = 'ecommerce.extensions.payment.views.paypal'
            with LogCapture(logger_name) as logger:
                self.mock_oauth2_response()
                self.mock_payment_creation_response(self.basket)
                self.processor.get_transaction_parameters(self.basket,
                                                          request=self.request)
                self._assert_error_page_redirect()

                logger.check(
                    (logger_name, 'INFO',
                     'Payment [{payment_id}] approved by payer [{payer_id}]'.
                     format(payment_id=self.PAYMENT_ID,
                            payer_id=self.PAYER_ID)),
                    (logger_name, 'ERROR',
                     'Unexpected error during basket retrieval while executing PayPal payment.'
                     ),
                )

    def _assert_error_page_redirect(self):
        """Verify redirection to the configured checkout error page after attempted failed payment execution."""
        response = self.client.get(reverse('paypal:execute'), self.RETURN_DATA)

        self.assertRedirects(response,
                             self.processor.error_url,
                             fetch_redirect_response=False)

    # TODO: Remove as a part of REVMI-124 as this tests a hacky solution
    # The problem is that orders are being created after payment processing, and the discount is not
    # saved in the database, so it needs to be calculated again in order to save the correct info to the
    # order. REVMI-124 will create the order before payment processing, when we have the discount context.
    @override_flag(DYNAMIC_DISCOUNT_FLAG, active=True)
    @httpretty.activate
    @responses.activate
    @mock.patch.object(PaymentProcessorResponse.objects, 'get')
    @mock.patch('ecommerce.extensions.payment.views.paypal.EdxRestApiClient')
    @mock.patch.object(PaypalPaymentExecutionView, 'handle_payment')
    def test_add_dynamic_discount_to_request_error(self, fake_handle_payment,
                                                   mocked_client,
                                                   basket_object):
        """
        Verify that we log a warning when the lms doesn't return a discount jwt
        """
        error_response = '<Response [401]>'
        mocked_client.return_value.user.return_value.course.return_value.get.side_effect = SlumberHttpBaseException(
            response=error_response)

        basket_object.return_value.basket = self.basket

        # login the user
        self.client.login(username=self.user.username, password=self.password)

        self.mock_oauth2_response()
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)

        logger_name = 'ecommerce.extensions.payment.views.paypal'
        with LogCapture(logger_name) as logger:
            with mock.patch.object(
                    SiteConfiguration,
                    'access_token',
                    return_value=self.mock_access_token_response()):
                self.client.get(reverse('paypal:execute'), self.RETURN_DATA)

            self.assertTrue(fake_handle_payment.called)
            logger.check_present(
                (logger_name, 'INFO',
                 'Payment [{payment_id}] approved by payer [{payer_id}]'.
                 format(payment_id=self.PAYMENT_ID, payer_id=self.PAYER_ID)),
                (logger_name, 'WARNING',
                 ('Failed to get discount jwt from LMS. '
                  '[http://lms.testserver.fake/api/discounts/] returned [{error}]'
                  ).format(error=error_response)),
            )

    @override_flag(DYNAMIC_DISCOUNT_FLAG, active=True)
    @httpretty.activate
    @responses.activate
    @mock.patch.object(PaymentProcessorResponse.objects, 'get')
    @mock.patch('ecommerce.extensions.payment.views.paypal.EdxRestApiClient')
    @mock.patch(
        'ecommerce.extensions.offer.dynamic_conditional_offer.jwt_decode_handler'
    )
    @mock.patch.object(PaypalPaymentExecutionView, 'handle_payment')
    def test_add_discount_to_request(self, fake_handle_payment,
                                     jwt_decode_handler, mocked_client,
                                     basket_object):
        """
        Verify that we correctly add the discount to the order
        """
        # The applicator will attempt to decode a jwt
        jwt_decode_handler.side_effect = _mock_jwt_decode_handler

        # Mock the call to lms
        expected_discount_percent = 15
        expected_discount_jwt = {
            'discount_applicable': True,
            'discount_percent': expected_discount_percent
        }
        mocked_client.return_value.user.return_value.course.return_value.get.return_value = {
            'discount_applicable': True,
            'jwt': expected_discount_jwt
        }

        # login the user
        self.client.login(username=self.user.username, password=self.password)

        basket_object.return_value.basket = self.basket

        self.mock_oauth2_response()

        # Create payment records
        self.mock_payment_creation_response(self.basket)
        self.processor.get_transaction_parameters(self.basket,
                                                  request=self.request)

        with mock.patch.object(SiteConfiguration,
                               'access_token',
                               return_value=self.mock_access_token_response()):
            self.client.get(reverse('paypal:execute'), self.RETURN_DATA)

        self.assertTrue(fake_handle_payment.called)
        self.assertTrue(
            Order.objects.get().total_incl_tax,
            Decimal(self.price) *
            (Decimal(expected_discount_percent) / Decimal(100.0)))