Beispiel #1
0
 def redirect_on_transaction_declined(self):
     redirect_url = get_payment_microfrontend_or_basket_url(self.request)
     redirect_url = add_utm_params_to_url(redirect_url,
                                          list(self.request.GET.items()))
     return JsonResponse({
         'redirectTo': redirect_url,
     }, status=400)
Beispiel #2
0
    def test_decline(self):
        """ Verify the view reports an error if the transaction is only authorized pending review. """
        basket = self._create_valid_basket()
        data = self._generate_data(basket.id)
        order_number = OrderNumberGenerator().order_number_from_basket_id(
            self.site.siteconfiguration.partner,
            basket.id,
        )
        # This response has been pruned to only the needed data.
        self._prep_request_success(
            """{"links":{"_self":{"href":"/pts/v2/payments/6021683934456376603262","method":"GET"}},"id":"6021683934456376603262","status":"DECLINED","error_information":{"reason":"PROCESSOR_DECLINED","message":"Decline - General decline of the card. No other information provided by the issuing bank."},"client_reference_information":{"code":"%s"},"processor_information":{"transaction_id":"460282531937765","network_transaction_id":"460282531937765","response_code":"005","avs":{"code":"D","code_raw":"D"},"card_verification":{"result_code":"M","result_code_raw":"M"}},"payment_information":{"account_features":{"category":"F"}}}"""
            % order_number  # pylint: disable=line-too-long
        )
        response = self.client.post(self.path, data)

        assert response.status_code == 400
        assert response['content-type'] == JSON

        request = RequestFactory(SERVER_NAME='testserver.fake').post(
            self.path, data)
        request.site = self.site
        assert json.loads(
            response.content
        )['redirectTo'] == get_payment_microfrontend_or_basket_url(request)

        # Ensure the basket is frozen
        basket = Basket.objects.get(pk=basket.pk)
        self.assertEqual(basket.status, Basket.MERGED)
        assert Basket.objects.count() == 2
Beispiel #3
0
 def redirect_on_transaction_declined(self):
     redirect_url = get_payment_microfrontend_or_basket_url(self.request)
     redirect_url = add_utm_params_to_url(redirect_url,
                                          list(self.request.GET.items()))
     # TODO: Remove as part of PCI-81
     redirect_url = add_flex_microform_flag_to_url(redirect_url,
                                                   self.request)
     return HttpResponseRedirect(redirect_url)
Beispiel #4
0
    def _redirect_response_to_basket_or_payment(self,
                                                request,
                                                invalid_code=None):
        redirect_url = get_payment_microfrontend_or_basket_url(request)
        redirect_url = add_utm_params_to_url(redirect_url,
                                             list(self.request.GET.items()))
        redirect_url = add_invalid_code_message_to_url(redirect_url,
                                                       invalid_code)

        return HttpResponseRedirect(redirect_url, status=303)
Beispiel #5
0
 def _redirect_response_to_basket_or_payment(self, request, skus):
     redirect_url = get_payment_microfrontend_or_basket_url(request)
     redirect_url = add_utm_params_to_url(redirect_url,
                                          list(self.request.GET.items()))
     # If a user is eligible and bucketed, REV1074 experiment information will be added to their url
     if waffle.flag_is_active(
             self.request, 'REV1074.enable_experiment'):  # pragma: no cover
         if skus:
             redirect_url = add_REV1074_information_to_url_if_eligible(
                 redirect_url, request, skus[0])
     return HttpResponseRedirect(redirect_url, status=303)
Beispiel #6
0
 def redirect_on_transaction_declined(self):
     redirect_url = get_payment_microfrontend_or_basket_url(self.request)
     redirect_url = add_utm_params_to_url(redirect_url,
                                          list(self.request.GET.items()))
     # TODO: Remove as part of PCI-81
     redirect_url = add_flex_microform_flag_to_url(redirect_url,
                                                   self.request,
                                                   force_flag=False)
     return JsonResponse({
         'redirectTo': redirect_url,
     }, status=400)
Beispiel #7
0
    def _redirect_response_to_basket_or_payment(self, request, skus, invalid_code=None):
        redirect_url = get_payment_microfrontend_or_basket_url(request)
        # If a user is eligible and bucketed, REV1074 experiment information will be added to their url
        REV1074_is_active = waffle.flag_is_active(self.request, 'REV1074.enable_experiment')
        if REV1074_is_active and skus and not invalid_code:  # pragma: no cover
            redirect_url = add_REV1074_information_to_url_if_eligible(redirect_url, request, skus[0])
            redirect_url += '?basket_id=' + str(request.basket.id)
        else:  # pragma: no cover
            redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items()))
        redirect_url = add_invalid_code_message_to_url(redirect_url, invalid_code)

        return HttpResponseRedirect(redirect_url, status=303)
Beispiel #8
0
    def _redirect_response_to_basket_or_payment(self, request, skus):
        redirect_url = get_payment_microfrontend_or_basket_url(request)
        # If a user is eligible and bucketed, REV1074 experiment information will be added to their url
        if waffle.flag_is_active(self.request, 'REV1074.enable_experiment') and skus:  # pragma: no cover
            redirect_url = add_REV1074_information_to_url_if_eligible(redirect_url, request, skus[0])
            redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items()))
            basket = 'basket_id=' + str(request.basket.id)
            has_params = urlparse(redirect_url).query
            redirect_url += '&' + basket if has_params else '?' + basket
        else:  # pragma: no cover
            redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items()))

        return HttpResponseRedirect(redirect_url, status=303)
Beispiel #9
0
    def post(self, request, *args, **kwargs):  # pylint: disable=unused-argument
        """Process a CyberSource merchant notification and place an order for paid products as appropriate."""
        notification = request.POST.dict()
        try:
            basket = self.validate_notification(notification)
            monitoring_utils.set_custom_metric('payment_response_validation', 'success')
        except DuplicateReferenceNumber:
            # CyberSource has told us that they've declined an attempt to pay
            # for an existing order. If this happens, we can redirect the browser
            # to the receipt page for the existing order.
            monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-receipt')
            return self.redirect_to_receipt_page(notification)
        except TransactionDeclined:
            # Declined transactions are the most common cause of errors during payment
            # processing and tend to be easy to correct (e.g., an incorrect CVV may have
            # been provided). The recovery path is not as clear for other exceptions,
            # so we let those drop through to the payment error page.
            self._merge_old_basket_into_new(request)

            messages.error(self.request, _('transaction declined'), extra_tags='transaction-declined-message')

            monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-payment-page')
            # TODO:
            # 1. There are sometimes messages from CyberSource that would make a more helpful message for users.
            # 2. We could have similar handling of other exceptions like UserCancelled and AuthorizationError

            redirect_url = get_payment_microfrontend_or_basket_url(self.request)
            return HttpResponseRedirect(redirect_url)

        except:  # pylint: disable=bare-except
            # logging handled by validate_notification, because not all exceptions are problematic
            monitoring_utils.set_custom_metric('payment_response_validation', 'redirect-to-error-page')
            return absolute_redirect(request, 'payment_error')

        try:
            order = self.create_order(request, basket, self._get_billing_address(notification))
            self.handle_post_order(order)
            return self.redirect_to_receipt_page(notification)
        except:  # pylint: disable=bare-except
            transaction_id, order_number, basket_id = self.get_ids_from_notification(notification)
            logger.exception(
                'Error processing order for transaction [%s], with order [%s] and basket [%d].',
                transaction_id,
                order_number,
                basket_id
            )
            return absolute_redirect(request, 'payment_error')
Beispiel #10
0
    def test_authorized_pending_review_request_reversal_failed(self):
        """ Verify the view reports an error if the transaction is only authorized pending review. """
        basket = self._create_valid_basket()
        data = self._generate_data(basket.id)
        order_number = OrderNumberGenerator().order_number_from_basket_id(
            self.site.siteconfiguration.partner,
            basket.id,
        )
        # This response has been pruned to only the needed data.
        self._prep_request_success(
            """{"links":{"_self":{"href":"/pts/v2/payments/6038898237296087603031","method":"GET"}},"id":"6038898237296087603031","submit_time_utc":"2020-10-28T12:57:04Z","status":"AUTHORIZED_PENDING_REVIEW","error_information":{"reason":"AVS_FAILED","message":"Soft Decline - The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the Address Verification Service (AVS) check."},"client_reference_information":{"code":"%s"},"processor_information":{"approval_code":"028252","transaction_id":"580302466249046","network_transaction_id":"580302466249046","response_code":"000","avs":{"code":"N","code_raw":"N"},"card_verification":{"result_code":"M","result_code_raw":"M"}},"payment_information":{"account_features":{"category":"C"}},"order_information":{"amount_details":{"authorized_amount":"25.00","currency":"USD"}}}"""
            % order_number  # pylint: disable=line-too-long
        )
        self._prep_request_invalid(
            """{"submitTimeUtc":"2020-09-30T18:53:23Z","status":"INVALID_REQUEST","reason":"DUPLICATE_REQUEST","message":"Declined - The\u00a0merchantReferenceCode\u00a0sent with this authorization request matches the merchantReferenceCode of another authorization request that you sent in the last 15 minutes."}""",  # pylint: disable=line-too-long
            '6038898237296087603031')

        response = self.client.post(self.path, data)

        assert response.status_code == 400
        assert response['content-type'] == JSON

        request = RequestFactory(SERVER_NAME='testserver.fake').post(
            self.path, data)
        request.site = self.site
        assert json.loads(
            response.content
        )['redirectTo'] == get_payment_microfrontend_or_basket_url(request)

        # Ensure the basket is frozen
        basket = Basket.objects.get(pk=basket.pk)
        self.assertEqual(basket.status, Basket.MERGED)
        assert Basket.objects.count() == 2

        # Ensure that 2 requests were sent to cybersource
        assert self.mock_cybersource_request.call_count == 2

        # Ensure that 2 requests and 2 responses were recorded as PaymentProcessorResponses
        assert PaymentProcessorResponse.objects.all().count() == 4
Beispiel #11
0
    def get(self, request):  # pylint: disable=too-many-statements
        """
        Looks up the passed code and adds the matching product to a basket,
        then applies the voucher and if the basket total is FREE places the order and
        enrolls the user in the course.
        """
        template_name = 'coupons/_offer_error.html'
        code = request.GET.get('code')
        sku = request.GET.get('sku')
        failure_url = request.GET.get('failure_url')
        site_configuration = request.site.siteconfiguration

        if not code:
            return render(request, template_name,
                          {'error': _('Code not provided.')})
        if not sku:
            return render(request, template_name,
                          {'error': _('SKU not provided.')})

        try:
            voucher = Voucher.objects.get(code=code)
        except Voucher.DoesNotExist:
            msg = 'No voucher found with code {code}'.format(code=code)
            return render(request, template_name, {'error': _(msg)})

        try:
            product = StockRecord.objects.get(partner_sku=sku).product
        except StockRecord.DoesNotExist:
            return render(request, template_name,
                          {'error': _('The product does not exist.')})

        valid_voucher, msg = voucher_is_valid(voucher, [product], request)
        if not valid_voucher:
            logger.warning(
                '[Code Redemption Failure] The voucher is not valid for this product. '
                'User: %s, Product: %s, Code: %s, Message: %s',
                request.user.username, product.id, voucher.code, msg)
            return render(request, template_name, {'error': msg})

        offer = voucher.best_offer
        if not offer.is_email_valid(request.user.email):
            logger.warning(
                '[Code Redemption Failure] Unable to apply offer because the user\'s email '
                'does not meet the domain requirements. '
                'User: %s, Offer: %s, Code: %s', request.user.username,
                offer.id, voucher.code)
            return render(
                request, template_name,
                {'error': _('You are not eligible to use this coupon.')})

        email_confirmation_response = get_redirect_to_email_confirmation_if_required(
            request, offer, product)
        if email_confirmation_response:
            return email_confirmation_response

        try:
            enterprise_customer = get_enterprise_customer_from_voucher(
                request.site, voucher)
        except EnterpriseDoesNotExist as e:
            # If an EnterpriseException is caught while pulling the EnterpriseCustomer, that means there's no
            # corresponding EnterpriseCustomer in the Enterprise service (which should never happen).
            logger.exception(six.text_type(e))
            return render(
                request, template_name, {
                    'error':
                    _('Couldn\'t find a matching Enterprise Customer for this coupon.'
                      )
                })

        if enterprise_customer and product.is_course_entitlement_product:
            return render(
                request, template_name, {
                    'error':
                    _('This coupon code is not valid for entitlement course product. Try a different course.'
                      )
                })

        if enterprise_customer is not None and enterprise_customer_user_needs_consent(
                request.site,
                enterprise_customer['id'],
                product.course.id,
                request.user.username,
        ):
            consent_token = get_enterprise_customer_data_sharing_consent_token(
                request.user.access_token, product.course.id,
                enterprise_customer['id'])
            received_consent_token = request.GET.get('consent_token')
            if received_consent_token:
                # If the consent token is set, then the user is returning from the consent view. Render out an error
                # if the computed token doesn't match the one received from the redirect URL.
                if received_consent_token != consent_token:
                    logger.warning(
                        '[Code Redemption Failure] Unable to complete code redemption because of '
                        'invalid consent. User: %s, Offer: %s, Code: %s',
                        request.user.username, offer.id, voucher.code)
                    return render(request, template_name, {
                        'error':
                        _('Invalid data sharing consent token provided.')
                    })
            else:
                # The user hasn't been redirected to the interstitial consent view to collect consent, so
                # redirect them now.
                redirect_url = get_enterprise_course_consent_url(
                    request.site,
                    code,
                    sku,
                    consent_token,
                    product.course.id,
                    enterprise_customer['id'],
                    failure_url=failure_url)
                return HttpResponseRedirect(redirect_url)

        try:
            basket = prepare_basket(request, [product], voucher)
        except AlreadyPlacedOrderException:
            msg = _('You have already purchased {course} seat.').format(
                course=product.course.name)
            return render(request, template_name, {'error': msg})

        if basket.total_excl_tax == 0:
            try:
                order = self.place_free_order(basket)
                return HttpResponseRedirect(
                    get_receipt_page_url(
                        site_configuration,
                        order.number,
                        disable_back_button=True,
                    ), )
            except:  # pylint: disable=bare-except
                logger.exception(
                    'Failed to create a free order for basket [%d]', basket.id)
                return absolute_redirect(self.request, 'checkout:error')

        if enterprise_customer:
            if is_voucher_applied(basket, voucher):
                message = _(
                    'A discount has been applied, courtesy of {enterprise_customer_name}.'
                ).format(
                    enterprise_customer_name=enterprise_customer.get('name'))
                messages.info(self.request, message)
            else:
                # Display a generic message to the user if a condition-specific
                # message has not already been added by an unsatified Condition class.
                if not messages.get_messages(self.request):
                    messages.warning(
                        self.request,
                        _('This coupon code is not valid for this course. Try a different course.'
                          ))
                self.request.basket.vouchers.remove(voucher)

        # The coupon_redeem_redirect query param is used to communicate to the Payment MFE that it may redirect
        # and should not display the payment form before making that determination.
        # TODO: It would be cleaner if the user could be redirected to their final destination up front.
        redirect_url = get_payment_microfrontend_or_basket_url(
            self.request) + "?coupon_redeem_redirect=1"
        return HttpResponseRedirect(redirect_url)
Beispiel #12
0
 def redirect_on_transaction_declined(self):
     redirect_url = get_payment_microfrontend_or_basket_url(self.request)
     return HttpResponseRedirect(redirect_url)