Beispiel #1
0
def get_redirect_to_email_confirmation_if_required(request, offer, product):
    """
    Render the email confirmation template if email confirmation is
    required to redeem the offer.

    We require email confirmation via account activation before an offer
    can be redeemed if the site is configured to require account activation
    or if the offer is restricted for use to learners with a specific
    email domain. The learner needs to activate their account before we allow
    them to redeem email domain-restricted offers, otherwise anyone could create
    an account using an email address with a privileged domain and use the coupon
    code associated with the offer.

    Arguments:
        request (HttpRequest): The current HttpRequest.
        offer (ConditionalOffer): The offer to be redeemed.
        product (Product): The

    Returns:
        HttpResponse or None: An HttpResponse that redirects to the email confirmation view if required.
    """
    require_account_activation = request.site.siteconfiguration.require_account_activation or offer.email_domains
    if require_account_activation and not request.user.account_details(request).get('is_active'):
        response = absolute_redirect(request, 'offers:email_confirmation')
        course_id = product.course and product.course.id
        if course_id:
            response['Location'] += '?{params}'.format(params=urlencode({'course_id': course_id}))
        return response
    return None
Beispiel #2
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 #3
0
    def verify_enterprise_needs(self, basket):
        failed_enterprise_consent_code = self.request.GET.get(
            CONSENT_FAILED_PARAM)
        if failed_enterprise_consent_code:
            messages.error(
                self.request,
                _("Could not apply the code '{code}'; it requires data sharing consent."
                  ).format(code=failed_enterprise_consent_code))

        if has_enterprise_offer(basket) and basket.total_incl_tax == Decimal(
                0):
            raise RedirectException(response=absolute_redirect(
                self.request, 'checkout:free-checkout'), )
Beispiel #4
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 #5
0
 def redirect_to_payment_error(self):
     return absolute_redirect(self.request, 'payment_error')
Beispiel #6
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)
        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.
            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.
            order_number = request.POST.get('req_reference_number')
            old_basket_id = OrderNumberGenerator().basket_id(order_number)
            old_basket = Basket.objects.get(id=old_basket_id)

            new_basket = Basket.objects.create(owner=old_basket.owner,
                                               site=request.site)

            # We intentionally avoid thawing the old basket here to prevent order
            # numbers from being reused. For more, refer to commit a1efc68.
            new_basket.merge(old_basket, add_quantities=False)
            logger.info(
                'Created new basket [%d] from old basket [%d] for declined transaction.',
                new_basket.id,
                old_basket_id,
            )

            message = _(
                'An error occurred while processing your payment. You have not been charged. '
                'Please double-check the information you provided and try again. '
                'For help, {link_start}contact support{link_end}.').format(
                    link_start='<a href="{}">'.format(
                        request.site.siteconfiguration.payment_support_url),
                    link_end='</a>',
                )

            messages.error(request, mark_safe(message))

            # TODO:
            # 1. TransactionDeclined message may be mishandled by Payment MFE since it contains HTML.
            # 2. There are sometimes messages from CyberSource that would make a more helpful message for users.
            # 3. We could have similar handling of other exceptions like UserCancelled and AuthorizationError
            return absolute_redirect(request, 'basket:summary')
        except:  # pylint: disable=bare-except
            # logging handled by validate_notification, because not all exceptions are problematic
            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')
    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.
            order_number = request.POST.get('req_reference_number')
            old_basket_id = OrderNumberGenerator().basket_id(order_number)
            old_basket = Basket.objects.get(id=old_basket_id)

            new_basket = Basket.objects.create(owner=old_basket.owner,
                                               site=request.site)

            # We intentionally avoid thawing the old basket here to prevent order
            # numbers from being reused. For more, refer to commit a1efc68.
            new_basket.merge(old_basket, add_quantities=False)
            logger.info(
                'Created new basket [%d] from old basket [%d] for declined transaction.',
                new_basket.id,
                old_basket_id,
            )

            messages.error(self.request,
                           _('transation 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

            return absolute_redirect(request, 'basket:summary')
        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')