Exemple #1
0
    def test_enterprise_contract_metadata_not_created(self):
        """
        Verify an orderline is NOT updated with calculated enterprise discount data
        if a product is fulfilled with a coupon for an enterprise customer but
        that coupon has NO `enterprise_contract_metadata` associated with it.
        """
        code, _ = self.prepare_enterprise_data(
            enterprise_customer_catalog=ENTERPRISE_CUSTOMER_CATALOG, )
        self.mock_assignable_enterprise_condition_calls(
            ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)

        self.request.user = self.user
        self.client.get(
            self.redeem_url_with_params(code=code,
                                        consent_token=consent_token))

        # Assert our orderline has nothing set for enterprise discount fields
        orderline = Order.objects.first().lines.last()
        assert orderline.effective_contract_discount_percentage is None
        assert orderline.effective_contract_discounted_price is None
Exemple #2
0
    def test_enterprise_contract_metadata_create_on_coupon_redemption(self):
        """
        Verify an orderline is updated with calculated enterprise discount data
        if a product is fulfilled with a coupon for an enterprise customer and
        that coupon has `enterprise_contract_metadata` associated with it.
        """
        code, _ = self.prepare_enterprise_data(
            enterprise_customer_catalog=ENTERPRISE_CUSTOMER_CATALOG,
            contract_discount_type=EnterpriseContractMetadata.FIXED,
            contract_discount_value=Decimal('1337.00'),
            prepaid_invoice_amount=Decimal('2000.00'),
        )
        self.mock_assignable_enterprise_condition_calls(
            ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)

        self.request.user = self.user
        self.client.get(
            self.redeem_url_with_params(code=code,
                                        consent_token=consent_token))

        # Assert our orderline has attributes updated with some decimal value
        orderline = Order.objects.first().lines.last()
        self.assertIsInstance(orderline.effective_contract_discount_percentage,
                              Decimal)
        self.assertIsInstance(orderline.effective_contract_discounted_price,
                              Decimal)
Exemple #3
0
    def test_enterprise_customer_successful_redemption(self):
        """ Verify the view redirects to LMS when valid consent is provided. """
        code = self.create_and_test_coupon_and_return_code(
            benefit_value=100,
            code='',
            enterprise_customer=ENTERPRISE_CUSTOMER)
        self.request.user = self.user
        self.mock_enrollment_api(self.request,
                                 self.user,
                                 self.course.id,
                                 is_active=False,
                                 mode=self.course_mode)
        self.mock_account_api(self.request,
                              self.user.username,
                              data={'is_active': True})
        self.mock_access_token_response()
        self.mock_specific_enterprise_customer_api(ENTERPRISE_CUSTOMER)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)

        self.assert_redemption_page_redirects(
            self.student_dashboard_url,
            target=status.HTTP_301_MOVED_PERMANENTLY,
            code=code,
            consent_token=consent_token,
        )
        last_request = httpretty.last_request()
        self.assertEqual(last_request.path, '/api/enrollment/v1/enrollment')
        self.assertEqual(last_request.method, 'POST')
Exemple #4
0
    def test_enterprise_customer_redirect_no_consent(self):
        """ Verify the view redirects to LMS when an enrollment code is provided. """
        code = self.create_and_test_coupon_and_return_code(
            benefit_value=100,
            code='',
            enterprise_customer=ENTERPRISE_CUSTOMER)
        self.request.user = self.user
        self.mock_enrollment_api(self.request,
                                 self.user,
                                 self.course.id,
                                 is_active=False,
                                 mode=self.course_mode)
        self.mock_account_api(self.request,
                              self.user.username,
                              data={'is_active': True})
        self.mock_access_token_response()
        self.mock_specific_enterprise_customer_api(ENTERPRISE_CUSTOMER)

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)
        expected_url = get_enterprise_course_consent_url(
            self.site, code, self.stock_record.partner_sku, consent_token,
            self.course.id, ENTERPRISE_CUSTOMER)

        response = self.client.get(self.redeem_url_with_params(code=code))
        self.assertEqual(response.status_code, status.HTTP_302_FOUND)
        self.assertEqual(response.url, expected_url)
Exemple #5
0
    def test_enterprise_customer_redirect_no_consent(self):
        """ Verify the view redirects to LMS when an enrollment code is provided. """
        code = self.prepare_enterprise_data(catalog=self.catalog)
        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)
        expected_url = get_enterprise_course_consent_url(
            self.site, code, self.stock_record.partner_sku, consent_token,
            self.course.id, ENTERPRISE_CUSTOMER)

        response = self.client.get(self.redeem_url_with_params(code=code))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, expected_url)
Exemple #6
0
    def test_enterprise_customer_successful_redemption(self):
        """ Verify the view redirects to LMS when valid consent is provided. """
        code = self.prepare_enterprise_data(catalog=self.catalog)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)

        self.assert_redirects_to_receipt_page(code=code,
                                              consent_token=consent_token)
        last_request = httpretty.last_request()
        self.assertEqual(last_request.path, '/api/enrollment/v1/enrollment')
        self.assertEqual(last_request.method, 'POST')
Exemple #7
0
    def test_enterprise_customer_successful_redemption(self):
        """ Verify the view redirects to LMS courseware page when valid consent is provided. """
        code, _ = self.prepare_enterprise_data(enterprise_customer_catalog=ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_assignable_enterprise_condition_calls(ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token,
            self.course.id,
            ENTERPRISE_CUSTOMER
        )

        response = self.redeem_coupon(code=code, consent_token=consent_token)
        self.assertRedirects(response, get_lms_courseware_url(self.course.id), fetch_redirect_response=False)

        last_request = httpretty.last_request()
        self.assertEqual(last_request.path, '/api/enrollment/v1/enrollment')
        self.assertEqual(last_request.method, 'POST')
Exemple #8
0
    def test_enterprise_customer_successful_redemption(self):
        """ Verify the view redirects to LMS when valid consent is provided. """
        Switch.objects.update_or_create(name=ENTERPRISE_OFFERS_SWITCH,
                                        defaults={'active': True})
        Switch.objects.update_or_create(
            name=ENTERPRISE_OFFERS_FOR_COUPONS_SWITCH,
            defaults={'active': True})
        code = self.prepare_enterprise_data(
            enterprise_customer_catalog=ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_assignable_enterprise_condition_calls(
            ENTERPRISE_CUSTOMER_CATALOG)
        self.mock_enterprise_learner_api_for_learner_with_no_enterprise()
        self.mock_enterprise_learner_post_api()

        consent_token = get_enterprise_customer_data_sharing_consent_token(
            self.request.user.access_token, self.course.id,
            ENTERPRISE_CUSTOMER)

        self.assert_redirects_to_receipt_page(code=code,
                                              consent_token=consent_token)
        last_request = httpretty.last_request()
        self.assertEqual(last_request.path, '/api/enrollment/v1/enrollment')
        self.assertEqual(last_request.method, 'POST')
Exemple #9
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)
Exemple #10
0
    def get(self, request):
        """
        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:
            return render(request, template_name, {'error': msg})

        offer = voucher.offers.first()
        if not offer.is_email_valid(request.user.email):
            return render(
                request, template_name,
                {'error': _('You are not eligible to use this coupon.')})

        email_confirmation_response = render_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(e.message)
            return render(
                request, template_name, {
                    'error':
                    _('Couldn\'t find a matching Enterprise Customer for this coupon.'
                      )
                })

        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:
                    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))
            except:  # pylint: disable=bare-except
                logger.exception(
                    'Failed to create a free order for basket [%d]', basket.id)
                return HttpResponseRedirect(reverse('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'))
                message = '<i class="fa fa-info-circle"></i> {}'.format(
                    message)
                messages.info(self.request, message, extra_tags='safe')
            else:
                messages.warning(
                    self.request,
                    _('This coupon code is not valid for this course. Try a different course.'
                      ))
                self.request.basket.vouchers.remove(voucher)

        return HttpResponseRedirect(reverse('basket:summary'))