Beispiel #1
0
 def record_payment(self, basket, handled_processor_response):
     self.emit_checkout_step_events(basket, handled_processor_response,
                                    self.payment_processor)
     track_segment_event(basket.site, basket.owner, 'Payment Info Entered',
                         {'checkout_id': basket.order_number})
     source_type, __ = SourceType.objects.get_or_create(
         name=self.payment_processor.NAME)
     total = handled_processor_response.total
     reference = handled_processor_response.transaction_id
     source = Source(source_type=source_type,
                     currency=handled_processor_response.currency,
                     amount_allocated=total,
                     amount_debited=total,
                     reference=reference,
                     label=handled_processor_response.card_number,
                     card_type=handled_processor_response.card_type)
     event_type, __ = PaymentEventType.objects.get_or_create(
         name=PaymentEventTypeName.PAID)
     payment_event = PaymentEvent(
         event_type=event_type,
         amount=total,
         reference=reference,
         processor_name=self.payment_processor.NAME)
     self.add_payment_source(source)
     self.add_payment_event(payment_event)
     audit_log('payment_received',
               amount=payment_event.amount,
               basket_id=basket.id,
               currency=source.currency,
               processor_name=payment_event.processor_name,
               reference=payment_event.reference,
               user_id=basket.owner.id)
Beispiel #2
0
def _use_payment_microfrontend(request):
    """
    Return whether the current request should use the payment MFE.
    """
    if (
            request.site.siteconfiguration.enable_microfrontend_for_basket_page and
            request.site.siteconfiguration.payment_microfrontend_url
    ):
        # Force the user into the MFE bucket for testing
        payment_mfe_bucket_forced = _force_payment_microfrontend_bucket(request)
        if payment_mfe_bucket_forced:
            bucket = PAYMENT_MFE_BUCKET
        else:
            # Bucket 50% of users to use the payment MFE for A/B testing.
            bucket = stable_bucketing_hash_group("payment-mfe", 2, request.user.username)

        payment_microfrontend_flag_enabled = waffle.flag_is_active(
            request,
            ENABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME
        )

        track_segment_event(
            request.site,
            request.user,
            'edx.bi.experiment.user.bucketed',
            {
                'bucket': bucket,
                'experiment': 'payment-mfe',
                'forcedIntoBucket': payment_mfe_bucket_forced,
                'paymentMfeEnabled': payment_microfrontend_flag_enabled,
            },
        )
        return bucket == PAYMENT_MFE_BUCKET and payment_microfrontend_flag_enabled
    else:
        return False
Beispiel #3
0
 def flush(self):
     """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each"""
     for line in self.all_lines():
         properties = translate_basket_line_for_segment(line)
         track_segment_event(self.site, self.owner, 'Product Removed',
                             properties)
     super(Basket, self).flush()  # pylint: disable=bad-super-call
Beispiel #4
0
def add_REV1074_information_to_url_if_eligible(redirect_url, request, sku):
    """
    For https://openedx.atlassian.net/browse/REV-1074 we are testing a mostly hardcoded version of the checkout page.
    We are trying to improve performance and measure if there is an effect on revenue.
    Here we determine which users are eligible to be in the experiment, then bucket the users
    into a treatment and control group, and send a log message to record this information for our experiment analysis
    """
    is_eligible_for_experiment = _is_eligible_for_REV1074_experiment(request, sku)
    bucket = stable_bucketing_hash_group('REV-1074', 2, request.user.username)
    route = bucket
    username = request.user.username
    basket = request.basket
    properties = {
        'experiment': 'static_checkout_page',
        'cart_id': basket.id
    }
    if not is_eligible_for_experiment:
        route = 0
        logger.info('REV1074: Should be omitted from experiment results: user [%s] with basket [%s].', username, basket)
        properties['bucket'] = 'not_in_experiment'
    elif is_eligible_for_experiment and bucket:
        logger.info('REV1074: Bucketed into treatment variation: user [%s] with basket [%s].', username, basket)
        properties['bucket'] = 'treatment'
    else:
        logger.info('REV1074: Bucketed into control variation: user [%s] with basket [%s].', username, basket)
        properties['bucket'] = 'control'

    track_segment_event(request.site, request.user, 'edx.bi.experiment.user.bucketed', properties)

    if route:
        redirect_url += sku + '.html'
    return redirect_url
Beispiel #5
0
    def get(self, request):
        # Send time when this view is called - https://openedx.atlassian.net/browse/REV-984
        properties = {'emitted_at': time.time()}
        track_segment_event(request.site, request.user, 'Basket Add Items View Called', properties)

        try:
            skus = self._get_skus(request)
            products = self._get_products(request, skus)
            voucher = self._get_voucher(request)

            logger.info('Starting payment flow for user [%s] for products [%s].', request.user.username, skus)

            available_products = self._get_available_products(request, products)
            self._set_email_preference_on_basket(request)

            try:
                prepare_basket(request, available_products, voucher)
            except AlreadyPlacedOrderException:
                return render(request, 'edx/error.html', {'error': _('You have already purchased these products')})

            return self._redirect_response_to_basket_or_payment(request)

        except BadRequestException as e:
            return HttpResponseBadRequest(six.text_type(e))
        except RedirectException as e:
            return e.response
Beispiel #6
0
    def handle_payment(self, response, basket):  # pylint: disable=arguments-differ
        """
        Handle any payment processing and record payment sources and events.

        This method is responsible for handling payment and recording the
        payment sources (using the add_payment_source method) and payment
        events (using add_payment_event) so they can be
        linked to the order when it is saved later on.
        """
        properties = {
            'basket_id': basket.id,
            'processor_name': self.payment_processor.NAME,
        }
        # If payment didn't go through, the handle_processor_response function will raise an error. We want to
        # send the event regardless of if the payment didn't go through.
        try:
            handled_processor_response = self.payment_processor.handle_processor_response(
                response, basket=basket)
        except Exception as ex:
            properties.update({
                'success': False,
                'payment_error': type(ex).__name__,
            })
            raise
        else:
            # We only record successful payments in the database.
            self.record_payment(basket, handled_processor_response)
            properties.update({
                'total': handled_processor_response.total,
                'success': True,
            })
        finally:
            track_segment_event(basket.site, basket.owner,
                                'Payment Processor Response', properties)
Beispiel #7
0
    def get(self, request, *args, **kwargs):
        basket = request.basket

        try:
            properties = {
                'cart_id':
                basket.id,
                'products': [
                    translate_basket_line_for_segment(line)
                    for line in basket.all_lines()
                ],
            }
            track_segment_event(request.site, request.user, 'Cart Viewed',
                                properties)

            properties = {'checkout_id': basket.order_number, 'step': 1}
            track_segment_event(request.site, request.user,
                                'Checkout Step Viewed', properties)
        except Exception:  # pylint: disable=broad-except
            logger.exception(
                'Failed to fire Cart Viewed event for basket [%d]', basket.id)

        if has_enterprise_offer(basket) and basket.total_incl_tax == Decimal(
                0):
            return redirect('checkout:free-checkout')
        else:
            return super(BasketSummaryView, self).get(request, *args, **kwargs)
Beispiel #8
0
def track_completed_order(sender, order=None, **kwargs):  # pylint: disable=unused-argument
    """Emit a tracking event when an order is placed."""
    if order.total_excl_tax <= 0:
        return

    properties = {
        'orderId':
        order.number,
        'total':
        str(order.total_excl_tax),
        'currency':
        order.currency,
        'products': [
            {
                # For backwards-compatibility with older events the `sku` field is (ab)used to
                # store the product's `certificate_type`, while the `id` field holds the product's
                # SKU. Marketing is aware that this approach will not scale once we start selling
                # products other than courses, and will need to change in the future.
                'id':
                line.partner_sku,
                'sku':
                mode_for_seat(line.product),
                'name':
                line.product.course.id
                if line.product.course else line.product.title,
                'price':
                str(line.line_price_excl_tax),
                'quantity':
                line.quantity,
                'category':
                line.product.get_product_class().name,
            } for line in order.lines.all()
        ],
    }
    track_segment_event(order.site, order.user, 'Order Completed', properties)
Beispiel #9
0
    def add_product(self, product, quantity=1, options=None):
        """ Add the indicated product to basket.

        Performs AbstractBasket add_product method and fires Google Analytics 'Product Added' event.
        """
        line, created = super(Basket,
                              self).add_product(product, quantity, options)  # pylint: disable=bad-super-call
        properties = translate_basket_line_for_segment(line)
        properties['cart_id'] = self.id
        track_segment_event(self.site, self.owner, 'Product Added', properties)
        return line, created
Beispiel #10
0
    def flush(self):
        """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each"""
        for line in self.all_lines():

            # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU
            # usage. Given that orders for free items are ignored, there is no need for these events.
            if line.stockrecord.price_excl_tax > 0:
                properties = translate_basket_line_for_segment(line)
                track_segment_event(self.site, self.owner, 'Product Removed',
                                    properties)
        super(Basket, self).flush()  # pylint: disable=bad-super-call
Beispiel #11
0
    def get(self, request):
        # Send time when this view is called - https://openedx.atlassian.net/browse/REV-984
        properties = {'emitted_at': time.time()}
        track_segment_event(request.site, request.user,
                            'Basket Add Items View Called', properties)

        try:
            skus = self._get_skus(request)
            products = self._get_products(request, skus)
            voucher = None
            invalid_code = None
            code = request.GET.get('code', None)
            try:
                voucher = self._get_voucher(request)
            except Voucher.DoesNotExist as e:  # pragma: nocover
                # Display an error message when an invalid code is passed as a parameter
                invalid_code = code

            logger.info(
                'Starting payment flow for user [%s] for products [%s].',
                request.user.username, skus)

            available_products = self._get_available_products(
                request, products)

            try:
                basket = prepare_basket(request, available_products, voucher)
            except AlreadyPlacedOrderException:
                return render(
                    request, 'edx/error.html',
                    {'error': _('You have already purchased these products')})

            self._set_email_preference_on_basket(request, basket)

            # Used basket object from request to allow enterprise offers
            # being applied on basket via BasketMiddleware
            self.verify_enterprise_needs(request.basket)
            if code and not request.basket.vouchers.exists():
                if not (len(available_products) == 1
                        and available_products[0].is_enrollment_code_product):
                    # Display an error message when an invalid code is passed as a parameter
                    invalid_code = code
            return self._redirect_response_to_basket_or_payment(
                request, invalid_code)

        except BadRequestException as e:
            return HttpResponseBadRequest(str(e))
        except RedirectException as e:
            return e.response
Beispiel #12
0
    def flush(self):
        """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each"""
        cached_response = RequestCache.get_cached_response(TEMPORARY_BASKET_CACHE_KEY)
        if cached_response.is_hit:
            # Do not track anything. This is a temporary basket calculation.
            return
        for line in self.all_lines():
            # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU
            # usage. Given that orders for free items are ignored, there is no need for these events.
            if line.stockrecord.price_excl_tax > 0:
                properties = translate_basket_line_for_segment(line)
                track_segment_event(self.site, self.owner, 'Product Removed', properties)

        # Call flush after we fetch all_lines() which is cleared during flush()
        super(Basket, self).flush()  # pylint: disable=bad-super-call
Beispiel #13
0
 def test_track_segment_event(self):
     """ The function should fire an event to Segment if the site is properly configured. """
     self.site_configuration.segment_key = 'fake-key'
     self.site_configuration.save()
     user, event, properties = self._get_generic_segment_event_parameters()
     user_tracking_id, ga_client_id, lms_ip = parse_tracking_context(user)
     context = {
         'ip': lms_ip,
         'Google Analytics': {
             'clientId': ga_client_id
         }
     }
     with mock.patch.object(Client, 'track') as mock_track:
         track_segment_event(self.site, user, event, properties)
         mock_track.assert_called_once_with(user_tracking_id, event, properties, context=context)
Beispiel #14
0
    def fire_segment_events(self, request, basket):
        try:
            properties = {
                'cart_id': basket.id,
                'products': [translate_basket_line_for_segment(line) for line in basket.all_lines()],
            }
            track_segment_event(request.site, request.user, 'Cart Viewed', properties)

            properties = {
                'checkout_id': basket.order_number,
                'step': 1
            }
            track_segment_event(request.site, request.user, 'Checkout Step Viewed', properties)
        except Exception:  # pylint: disable=broad-except
            logger.exception('Failed to fire Cart Viewed event for basket [%d]', basket.id)
Beispiel #15
0
def track_completed_refund(sender, refund=None, **kwargs):  # pylint: disable=unused-argument
    """Emit a tracking event when a refund is completed."""
    if refund.total_credit_excl_tax <= 0:
        return

    properties = {
        'orderId':
        refund.order.number,
        'products': [{
            'id': line.order_line.partner_sku,
            'quantity': line.quantity,
        } for line in refund.lines.all()],
    }
    track_segment_event(refund.order.site, refund.user, 'Order Refunded',
                        properties)
Beispiel #16
0
    def add_product(self, product, quantity=1, options=None):
        """ Add the indicated product to basket.

        Performs AbstractBasket add_product method and fires Google Analytics 'Product Added' event.
        """
        line, created = super(Basket,
                              self).add_product(product, quantity, options)  # pylint: disable=bad-super-call

        # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU
        # usage. Given that orders for free items are ignored, there is no need for these events.
        if line.stockrecord.price_excl_tax > 0:
            properties = translate_basket_line_for_segment(line)
            properties['cart_id'] = self.id
            track_segment_event(self.site, self.owner, 'Product Added',
                                properties)

        return line, created
Beispiel #17
0
    def flush(self):
        """Remove all products in basket and fire Segment 'Product Removed' Analytic event for each"""
        cached_response = DEFAULT_REQUEST_CACHE.get_cached_response(
            TEMPORARY_BASKET_CACHE_KEY)
        if cached_response.is_found:
            # Do not track anything. This is a temporary basket calculation.
            return
        product_removed_event_fired = False
        for line in self.all_lines():
            # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU
            # usage. Given that orders for free items are ignored, there is no need for these events.
            if line.stockrecord.price_excl_tax > 0:
                properties = translate_basket_line_for_segment(line)
                track_segment_event(self.site, self.owner, 'Product Removed',
                                    properties)
                product_removed_event_fired = True

        # Validate we sent an event for > 0 products to check if the bundle event is even necessary
        if product_removed_event_fired:
            try:
                bundle_id = BasketAttribute.objects.get(
                    basket=self, attribute_type__name=BUNDLE).value_text
                program = get_program(bundle_id, self.site.siteconfiguration)
                bundle_properties = {
                    'bundle_id': bundle_id,
                    'title': program.get('title'),
                    'total_price': self.total_excl_tax,
                    'quantity': self.lines.count(),
                }
                if program.get(
                        'type_attrs',
                    {}).get('slug') and program.get('marketing_slug'):
                    bundle_properties['marketing_slug'] = (
                        program['type_attrs']['slug'] + '/' +
                        program.get('marketing_slug'))
                track_segment_event(self.site, self.owner,
                                    'edx.bi.ecommerce.basket.bundle_removed',
                                    bundle_properties)
            except BasketAttribute.DoesNotExist:
                # Nothing to do here. It's not a bundle ¯\_(ツ)_/¯
                pass

        # Call flush after we fetch all_lines() which is cleared during flush()
        super(Basket, self).flush()  # pylint: disable=bad-super-call
Beispiel #18
0
    def get(self, request, *args, **kwargs):
        basket = request.basket

        try:
            properties = {
                'cart_id':
                basket.id,
                'products': [
                    translate_basket_line_for_segment(line)
                    for line in basket.all_lines()
                ],
            }
            track_segment_event(request.site, request.user, 'Cart Viewed',
                                properties)
        except Exception:  # pylint: disable=broad-except
            logger.exception(
                'Failed to fire Cart Viewed event for basket [%d]', basket.id)

        return super(BasketSummaryView, self).get(request, *args, **kwargs)
Beispiel #19
0
    def test_track_segment_event_without_segment_key(self):
        """ If the site has no Segment key, the function should log a debug message and NOT send an event."""
        self.site_configuration.segment_key = None
        self.site_configuration.save()

        with mock.patch('logging.Logger.debug') as mock_debug:
            msg = 'Event [foo] was NOT fired because no Segment key is set for site configuration [{}]'
            msg = msg.format(self.site_configuration.pk)
            self.assertEqual(track_segment_event(self.site, self.create_user(), 'foo', {}), (False, msg))
            mock_debug.assert_called_with(msg)
Beispiel #20
0
    def add_product(self, product, quantity=1, options=None):
        """
        Add the indicated product to basket.

        Performs AbstractBasket add_product method and fires Google Analytics 'Product Added' event.
        """
        line, created = super(Basket, self).add_product(product, quantity, options)  # pylint: disable=bad-super-call
        cached_response = DEFAULT_REQUEST_CACHE.get_cached_response(TEMPORARY_BASKET_CACHE_KEY)
        if cached_response.is_found:
            # Do not track anything. This is a temporary basket calculation.
            return line, created

        # Do not fire events for free items. The volume we see for edX.org leads to a dramatic increase in CPU
        # usage. Given that orders for free items are ignored, there is no need for these events.
        if line.stockrecord.price_excl_tax > 0:
            properties = translate_basket_line_for_segment(line)
            properties['cart_id'] = self.id
            track_segment_event(self.site, self.owner, 'Product Added', properties)
        return line, created
Beispiel #21
0
    def get(self, request, *args, **kwargs):
        basket = request.basket

        try:
            properties = {
                'cart_id':
                basket.id,
                'products': [
                    translate_basket_line_for_segment(line)
                    for line in basket.all_lines()
                ],
            }
            track_segment_event(request.site, request.user, 'Cart Viewed',
                                properties)

            properties = {'checkout_id': basket.order_number, 'step': 1}
            track_segment_event(request.site, request.user,
                                'Checkout Step Viewed', properties)
        except Exception:  # pylint: disable=broad-except
            logger.exception(
                'Failed to fire Cart Viewed event for basket [%d]', basket.id)

        if has_enterprise_offer(basket) and basket.total_incl_tax == Decimal(
                0):
            return redirect('checkout:free-checkout')
        else:

            #  lumsx is giving a thirdparty method for payment rather than a gateway so had to make a minimal
            #  processor and integerate the API, if client side processor matches with the site configurations
            #  than move forward towards API
            configuration_helpers = request.site.siteconfiguration.edly_client_theme_branding_settings
            custom_processor_name = configuration_helpers.get(
                'PAYMENT_PROCESSOR_NAME')
            if custom_processor_name == self.request.site.siteconfiguration.client_side_payment_processor:
                # return LumsxpayExecutionView.get_voucher_api(request)
                return redirect_to_referrer(self.request, 'lumsxpay:execute')

            return super(BasketSummaryView, self).get(request, *args, **kwargs)
Beispiel #22
0
    def test_track_segment_event(self):
        """ The function should fire an event to Segment if the site is properly configured. """
        properties = {'key': 'value'}
        self.site_configuration.segment_key = 'fake-key'
        self.site_configuration.save()
        user = self.create_user(
            tracking_context={
                'ga_client_id': 'test-client-id',
                'lms_user_id': 'foo',
                'lms_ip': '18.0.0.1',
            }
        )
        user_tracking_id, ga_client_id, lms_ip = parse_tracking_context(user)
        context = {
            'ip': lms_ip,
            'Google Analytics': {
                'clientId': ga_client_id
            }
        }
        event = 'foo'

        with mock.patch.object(Client, 'track') as mock_track:
            track_segment_event(self.site, user, event, properties)
            mock_track.assert_called_once_with(user_tracking_id, event, properties, context=context)
Beispiel #23
0
    def emit_checkout_step_events(self, basket, handled_processor_response, payment_processor):
        """ Emit events necessary to track the user in the checkout funnel. """

        properties = {
            'checkout_id': basket.order_number,
            'step': 1,
            'payment_method': '{} | {}'.format(handled_processor_response.card_type, payment_processor.NAME)
        }
        track_segment_event(basket.site, basket.owner, 'Checkout Step Completed', properties)

        properties['step'] = 2
        track_segment_event(basket.site, basket.owner, 'Checkout Step Viewed', properties)
        track_segment_event(basket.site, basket.owner, 'Checkout Step Completed', properties)
Beispiel #24
0
def track_completed_order(sender, order=None, **kwargs):  # pylint: disable=unused-argument
    """
    Emit a tracking event when
    1. An order is placed OR
    2. An enrollment code purchase order is placed.
    """
    if order.total_excl_tax <= 0:
        return

    properties = {
        'orderId':
        order.number,
        'total':
        str(order.total_excl_tax),
        # For Rockerbox integration, we need a field named revenue since they cannot parse a field named total.
        # TODO: DE-1188: Remove / move Rockerbox integration code.
        'revenue':
        str(order.total_excl_tax),
        'currency':
        order.currency,
        'discount':
        str(order.total_discount_incl_tax),
        'products': [
            {
                # For backwards-compatibility with older events the `sku` field is (ab)used to
                # store the product's `certificate_type`, while the `id` field holds the product's
                # SKU. Marketing is aware that this approach will not scale once we start selling
                # products other than courses, and will need to change in the future.
                'id':
                line.partner_sku,
                'sku':
                mode_for_product(line.product),
                'name':
                line.product.course.id
                if line.product.course else line.product.title,
                'price':
                str(line.line_price_excl_tax),
                'quantity':
                line.quantity,
                'category':
                line.product.get_product_class().name,
            } for line in order.lines.all()
        ],
    }
    if order.user:
        properties['email'] = order.user.email

    for line in order.lines.all():
        if line.product.is_enrollment_code_product:
            # Send analytics events to track bulk enrollment code purchases.
            track_segment_event(order.site, order.user,
                                'Bulk Enrollment Codes Order Completed',
                                properties)
            return

        if line.product.is_coupon_product:
            return

    voucher = order.basket_discounts.filter(voucher_id__isnull=False).first()
    coupon = voucher.voucher_code if voucher else None
    properties['coupon'] = coupon

    try:
        bundle_id = BasketAttribute.objects.get(
            basket=order.basket, attribute_type__name=BUNDLE).value_text
        program = get_program(bundle_id, order.basket.site.siteconfiguration)
        if len(order.lines.all()) < len(program.get('courses')):
            variant = 'partial'
        else:
            variant = 'full'
        bundle_product = {
            'id': bundle_id,
            'price': '0',
            'quantity': str(len(order.lines.all())),
            'category': 'bundle',
            'variant': variant,
            'name': program.get('title')
        }
        properties['products'].append(bundle_product)
    except BasketAttribute.DoesNotExist:
        logger.info(
            'There is no program or bundle associated with order number %s',
            order.number)

    track_segment_event(order.site, order.user, 'Order Completed', properties)
Beispiel #25
0
def prepare_basket(request, products, voucher=None):
    """
    Create or get the basket, add products, apply a voucher, and record referral data.

    Existing baskets are merged. Specified products will
    be added to the remaining open basket. If voucher is passed, all existing
    vouchers added to the basket are removed because we allow only one voucher per basket.
    Vouchers are not applied if an enrollment code product is in the basket.

    Arguments:
        request (Request): The request object made to the view.
        products (List): List of products to be added to the basket.
        voucher (Voucher): Voucher to apply to the basket.

    Returns:
        basket (Basket): Contains the product to be redeemed and the Voucher applied.
    """
    basket = Basket.get_basket(request.user, request.site)
    basket_add_enterprise_catalog_attribute(basket, request.GET)
    basket.flush()
    basket.save()
    basket_addition = get_class('basket.signals', 'basket_addition')
    already_purchased_products = []
    bundle = request.GET.get('bundle')

    _set_basket_bundle_status(bundle, basket)

    if request.site.siteconfiguration.enable_embargo_check:
        if not embargo_check(request.user, request.site, products):
            messages.error(
                request,
                _('Due to export controls, we cannot allow you to access this course at this time.'
                  ))
            logger.warning(
                'User [%s] blocked by embargo check, not adding products to basket',
                request.user.username)
            return basket

    is_multi_product_basket = len(products) > 1
    for product in products:
        # Multiple clicks can try adding twice, return if product is seat already in basket
        if is_duplicate_seat_attempt(basket, product):
            logger.info(
                'User [%s] repeated request to add [%s] seat of course [%s], will ignore',
                request.user.username, mode_for_product(product),
                product.course_id)
            return basket

        if product.is_enrollment_code_product or \
                not UserAlreadyPlacedOrder.user_already_placed_order(user=request.user,
                                                                     product=product, site=request.site):
            basket.add_product(product, 1)
            # Call signal handler to notify listeners that something has been added to the basket
            basket_addition.send(
                sender=basket_addition,
                product=product,
                user=request.user,
                request=request,
                basket=basket,
                is_multi_product_basket=is_multi_product_basket)
        else:
            already_purchased_products.append(product)
            logger.warning(
                'User [%s] attempted to repurchase the [%s] seat of course [%s]',
                request.user.username, mode_for_product(product),
                product.course_id)
    if already_purchased_products and basket.is_empty:
        raise AlreadyPlacedOrderException

    # Waiting to check and send segment event until after products are added into the basket
    # just in case the AlreadyPlacedOrderException is raised
    if bundle:
        program = get_program(bundle, request.site.siteconfiguration)
        bundle_properties = {
            'bundle_id': bundle,
            'cart_id': basket.id,
            'title': program.get('title'),
            'total_price': basket.total_excl_tax,
            'quantity': basket.lines.count(),
        }
        if program.get('type_attrs',
                       {}).get('slug') and program.get('marketing_slug'):
            bundle_properties['marketing_slug'] = program['type_attrs'][
                'slug'] + '/' + program.get('marketing_slug')
        track_segment_event(request.site, request.user,
                            'edx.bi.ecommerce.basket.bundle_added',
                            bundle_properties)

    if len(products) == 1 and products[0].is_enrollment_code_product:
        basket.clear_vouchers()
    elif voucher or basket.vouchers.exists():
        voucher = voucher or basket.vouchers.first()
        basket.clear_vouchers()
        is_valid, message = validate_voucher(voucher, request.user, basket,
                                             request.site)
        if is_valid:
            apply_voucher_on_basket_and_check_discount(voucher, request,
                                                       basket)
        else:
            logger.warning(
                '[Code Redemption Failure] The voucher is not valid for this basket. '
                'User: %s, Basket: %s, Code: %s, Message: %s',
                request.user.username, request.basket.id, voucher.code,
                message)

    attribute_cookie_data(basket, request)
    return basket