Example #1
0
    def test_tracking_context(self):
        """
        Ensure the tracking context is set up in the api client correctly and
        automatically.
        """
        with freeze_time('2015-7-2'):
            # fake an E-Commerce API request.
            httpretty.register_uri(
                httpretty.POST,
                '{}/baskets/1/'.format(settings.ECOMMERCE_API_URL.strip('/')),
                status=200, body='{}',
                adding_headers={'Content-Type': JSON}
            )

            mock_tracker = mock.Mock()
            mock_tracker.resolve_context = mock.Mock(return_value={'client_id': self.TEST_CLIENT_ID, 'ip': '127.0.0.1'})
            with mock.patch('openedx.core.djangoapps.commerce.utils.tracker.get_tracker', return_value=mock_tracker):
                ecommerce_api_client(self.user).baskets(1).post()

            # Verify the JWT includes the tracking context for the user
            actual_header = httpretty.last_request().headers['Authorization']

            claims = {
                'tracking_context': {
                    'lms_user_id': self.user.id,
                    'lms_client_id': self.TEST_CLIENT_ID,
                    'lms_ip': '127.0.0.1',
                }
            }
            expected_jwt = create_jwt_for_user(self.user, additional_claims=claims)
            expected_header = u'JWT {}'.format(expected_jwt)
            self.assertEqual(actual_header, expected_header)
Example #2
0
    def test_tracking_context(self):
        """
        Ensure the tracking context is set up in the api client correctly and
        automatically.
        """
        with freeze_time('2015-7-2'):
            # fake an E-Commerce API request.
            httpretty.register_uri(
                httpretty.POST,
                '{}/baskets/1/'.format(settings.ECOMMERCE_API_URL.strip('/')),
                status=200, body='{}',
                adding_headers={'Content-Type': JSON}
            )

            mock_tracker = mock.Mock()
            mock_tracker.resolve_context = mock.Mock(return_value={'client_id': self.TEST_CLIENT_ID, 'ip': '127.0.0.1'})
            with mock.patch('openedx.core.djangoapps.commerce.utils.tracker.get_tracker', return_value=mock_tracker):
                ecommerce_api_client(self.user).baskets(1).post()

            # Verify the JWT includes the tracking context for the user
            actual_header = httpretty.last_request().headers['Authorization']

            claims = {
                'tracking_context': {
                    'lms_user_id': self.user.id,
                    'lms_client_id': self.TEST_CLIENT_ID,
                    'lms_ip': '127.0.0.1',
                }
            }
            expected_jwt = create_jwt_for_user(self.user, additional_claims=claims)
            expected_header = u'JWT {}'.format(expected_jwt)
            self.assertEqual(actual_header, expected_header)
Example #3
0
def get_course_final_price(user, sku, course_price):
    """
    Return the course's discounted price for a user if user is eligible for any otherwise return course original price.
    """
    price_details = {}
    try:
        price_details = ecommerce_api_client(user).baskets.calculate.get(
            sku=[sku],
            username=user.username,
        )
    except (SlumberBaseException, ConnectionError, Timeout) as exc:
        LOGGER.info(
            '[e-commerce calculate endpoint] Exception raise for sku [%s] - user [%s] and exception: %s',
            sku, user.username, str(exc))

    LOGGER.info(
        '[e-commerce calculate endpoint] The discounted price for sku [%s] and user [%s] is [%s]',
        sku, user.username, price_details.get('total_incl_tax'))
    result = price_details.get('total_incl_tax', course_price)

    # When ecommerce price has zero cents, 'result' gets 149.0
    # As per REV-2260: if zero cents, then only show dollars
    if int(result) == result:
        result = int(result)

    return result
Example #4
0
def get_order(user, order_id):
    try:
        order = ecommerce_api_client(user).orders(order_id).get()
    except ConnectionError as e:
        order = None
        logger.exception(e)
    return order
Example #5
0
 def get(self, request, number):  # pylint:disable=unused-argument
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).orders(number).get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #6
0
def get_credit_provider_attribute_values(course_key, attribute_name):
    """Get the course information from ecommerce and parse the data to get providers.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        attribute_name (String): Name of the attribute of credit provider.

    Returns:
        List of provided credit provider attribute values.
    """
    course_id = six.text_type(course_key)
    credit_config = CreditConfig.current()

    cache_key = None
    attribute_values = None

    if credit_config.is_cache_enabled:
        cache_key = '{key_prefix}.{course_key}.{attribute_name}'.format(
            key_prefix=credit_config.CACHE_KEY,
            course_key=course_id,
            attribute_name=attribute_name)
        attribute_values = cache.get(cache_key)

    if attribute_values is not None:
        return attribute_values

    try:
        user = User.objects.get(
            username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
        response = ecommerce_api_client(user).courses(course_id).get(
            include_products=1)
    except Exception:  # pylint: disable=broad-except
        log.exception(
            u"Failed to receive data from the ecommerce course API for Course ID '%s'.",
            course_id)
        return attribute_values

    if not response:
        log.info(
            u"No Course information found from ecommerce API for Course ID '%s'.",
            course_id)
        return attribute_values

    provider_ids = []
    for product in response.get('products'):
        provider_ids += [
            attr.get('value') for attr in product.get('attribute_values')
            if attr.get('name') == 'credit_provider'
        ]

    attribute_values = []
    credit_providers = CreditProvider.get_credit_providers()
    for provider in credit_providers:
        if provider['id'] in provider_ids:
            attribute_values.append(provider[attribute_name])

    if credit_config.is_cache_enabled:
        cache.set(cache_key, attribute_values, credit_config.cache_ttl)

    return attribute_values
Example #7
0
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    user_orders = []
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration, 'orders', api=api, querystring=user_query, cache_key=cache_key
    )
    log.info('----------------------------------------Orders------------------------------------%s', commerce_user_orders)
    for order in commerce_user_orders:
        if order['status'].lower() == 'complete':
            date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
            order_data = {
                'number': order['number'],
                'price': order['total_excl_tax'],
                'order_date': strftime_localized(date_placed, 'SHORT_DATE'),
                'receipt_url': EcommerceService().get_receipt_page_url(order['number']),
                'lines': order['lines'],
            }
            user_orders.append(order_data)

    return user_orders
Example #8
0
def checkout_with_ecommerce_service(user, course_key, course_mode, processor):
    """ Create a new basket and trigger immediate checkout, using the E-Commerce API. """
    course_id = unicode(course_key)
    try:
        api = ecommerce_api_client(user)
        # Make an API call to create the order and retrieve the results
        result = api.baskets.post({
            'products': [{
                'sku': course_mode.sku
            }],
            'checkout': True,
            'payment_processor_name': processor
        })

        # Pass the payment parameters directly from the API response.
        return result.get('payment_data')
    except SlumberBaseException:
        params = {
            'username': user.username,
            'mode': course_mode.slug,
            'course_id': course_id
        }
        log.exception(
            'Failed to create order for %(username)s %(mode)s mode of %(course_id)s',
            params)
        raise
    finally:
        audit_log('checkout_requested',
                  course_id=course_id,
                  mode=course_mode.slug,
                  processor_name=processor,
                  user_id=user.id)
Example #9
0
def get_course_final_price(user, sku, course_price):
    """
    Return the course's discounted price for a user if user is eligible for any otherwise return course original price.
    """
    price_details = {}
    try:
        price_details = ecommerce_api_client(user).baskets.calculate.get(
            sku=[sku],
            username=user.username,
        )
    except (SlumberBaseException, ConnectionError, Timeout) as exc:
        LOGGER.info(
            '[e-commerce calculate endpoint] Exception raise for sku [%s] - user [%s] and exception: %s',
            sku,
            user.username,
            str(exc)
        )

    LOGGER.info(
        '[e-commerce calculate endpoint] The discounted price for sku [%s] and user [%s] is [%s]',
        sku,
        user.username,
        price_details.get('total_incl_tax')
    )
    return price_details.get('total_incl_tax', course_price)
Example #10
0
def checkout_with_ecommerce_service(user, course_key, course_mode, processor):
    """ Create a new basket and trigger immediate checkout, using the E-Commerce API. """
    course_id = unicode(course_key)
    try:
        api = ecommerce_api_client(user)
        # Make an API call to create the order and retrieve the results
        result = api.baskets.post({
            'products': [{'sku': course_mode.sku}],
            'checkout': True,
            'payment_processor_name': processor
        })

        # Pass the payment parameters directly from the API response.
        return result.get('payment_data')
    except SlumberBaseException:
        params = {'username': user.username, 'mode': course_mode.slug, 'course_id': course_id}
        log.exception('Failed to create order for %(username)s %(mode)s mode of %(course_id)s', params)
        raise
    finally:
        audit_log(
            'checkout_requested',
            course_id=course_id,
            mode=course_mode.slug,
            processor_name=processor,
            user_id=user.id
        )
Example #11
0
def get_order(user, order_id):
    try:
        order = ecommerce_api_client(user).orders(order_id).get()
    except ConnectionError as e:
        order = None
        logger.exception(e)
    return order
Example #12
0
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    user_orders = []
    allowed_course_modes = ['professional', 'verified', 'credit']
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(
        user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(commerce_configuration,
                                            user,
                                            'orders',
                                            api=api,
                                            querystring=user_query,
                                            cache_key=cache_key)

    for order in commerce_user_orders:
        if order['status'].lower() == 'complete':
            for line in order['lines']:
                product = line.get('product')
                if product:
                    for attribute in product['attribute_values']:
                        if attribute[
                                'name'] == 'certificate_type' and attribute[
                                    'value'] in allowed_course_modes:
                            try:
                                date_placed = datetime.strptime(
                                    order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
                                order_data = {
                                    'number':
                                    order['number'],
                                    'price':
                                    order['total_excl_tax'],
                                    'title':
                                    order['lines'][0]['title'],
                                    'order_date':
                                    strftime_localized(
                                        date_placed.replace(tzinfo=pytz.UTC),
                                        'SHORT_DATE'),
                                    'receipt_url':
                                    EcommerceService().get_receipt_page_url(
                                        order['number'])
                                }
                                user_orders.append(order_data)
                            except KeyError:
                                log.exception('Invalid order structure: %r',
                                              order)
                                return no_data

    return user_orders
Example #13
0
 def get(self, request, *_args, **kwargs):
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #14
0
 def get(self, request, number):  # pylint:disable=unused-argument
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).orders(number).get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #15
0
def refund_order_voucher(sender, course_enrollment, skip_refund=False, **kwargs):  # pylint: disable=unused-argument
    """
        Call the /api/v2/enterprise/coupons/create_refunded_voucher/ API to create new voucher and assign it to user.
    """

    if skip_refund:
        return
    if not course_enrollment.refundable():
        return
    if not EnterpriseCourseEnrollment.objects.filter(
        enterprise_customer_user__user_id=course_enrollment.user_id,
        course_id=str(course_enrollment.course.id)
    ).exists():
        return

    service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    client = ecommerce_api_client(service_user)
    order_number = course_enrollment.get_order_attribute_value('order_number')
    if order_number:
        error_message = u"Encountered {} from ecommerce while creating refund voucher. Order={}, enrollment={}, user={}"
        try:
            client.enterprise.coupons.create_refunded_voucher.post({"order": order_number})
        except HttpClientError as ex:
            log.info(
                error_message.format(type(ex).__name__, order_number, course_enrollment, course_enrollment.user)
            )
        except Exception as ex:  # pylint: disable=broad-except
            log.exception(
                error_message.format(type(ex).__name__, order_number, course_enrollment, course_enrollment.user)
            )
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    user_orders = []
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration, 'orders', api=api, querystring=user_query, cache_key=cache_key
    )

    for order in commerce_user_orders:
        if order['status'].lower() == 'complete':
            date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
            order_data = {
                'number': order['number'],
                'price': order['total_excl_tax'],
                'order_date': strftime_localized(date_placed, 'SHORT_DATE'),
                'receipt_url': EcommerceService().get_receipt_page_url(order['number']),
                'lines': order['lines'],
            }
            user_orders.append(order_data)

    return user_orders
Example #17
0
 def get(self, request, *_args, **kwargs):
     """ HTTP handler. """
     try:
         order = ecommerce_api_client(request.user).baskets(kwargs['basket_id']).order.get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #18
0
    def test_tracking_context(self):
        """
        Ensure the tracking context is set up in the api client correctly and
        automatically.
        """

        # fake an ecommerce api request.
        httpretty.register_uri(httpretty.POST,
                               '{}/baskets/1/'.format(TEST_API_URL),
                               status=200,
                               body='{}',
                               adding_headers={'Content-Type': JSON})

        mock_tracker = mock.Mock()
        mock_tracker.resolve_context = mock.Mock(return_value={
            'client_id': self.TEST_CLIENT_ID,
            'ip': '127.0.0.1'
        })
        with mock.patch(
                'openedx.core.djangoapps.commerce.utils.tracker.get_tracker',
                return_value=mock_tracker):
            ecommerce_api_client(self.user).baskets(1).post()

        # make sure the request's JWT token payload included correct tracking context values.
        actual_header = httpretty.last_request().headers['Authorization']
        expected_payload = {
            'username':
            self.user.username,
            'full_name':
            self.user.profile.name,
            'email':
            self.user.email,
            'iss':
            settings.JWT_AUTH['JWT_ISSUER'],
            'exp':
            datetime.datetime.utcnow() +
            datetime.timedelta(seconds=settings.JWT_AUTH['JWT_EXPIRATION']),
            'tracking_context': {
                'lms_user_id': self.user.id,  # pylint: disable=no-member
                'lms_client_id': self.TEST_CLIENT_ID,
                'lms_ip': '127.0.0.1',
            },
        }

        expected_header = 'JWT {}'.format(
            jwt.encode(expected_payload, TEST_API_SIGNING_KEY))
        self.assertEqual(actual_header, expected_header)
 def test_client_passed(self):
     """ Verify that when API client is passed edx_rest_api_client is not
     used.
     """
     program_config = self.create_programs_config()
     api = ecommerce_api_client(self.user)
     with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init:
         get_edx_api_data(program_config, self.user, 'orders', api=api)
         self.assertFalse(mock_init.called)
Example #20
0
def list_receipts(request):
    api = ecommerce_api_client(request.user)
    try:
        order_history = api.orders().get()
    except ConnectionError as e:
        logger.exception(e)
        order_history = []

    return render_to_response('payment/list_orders.html', {"order_history": order_history})
 def test_client_passed(self):
     """ Verify that when API client is passed edx_rest_api_client is not
     used.
     """
     program_config = self.create_programs_config()
     api = ecommerce_api_client(self.user)
     with mock.patch('openedx.core.lib.edx_api_utils.EdxRestApiClient.__init__') as mock_init:
         get_edx_api_data(program_config, self.user, 'orders', api=api)
         self.assertFalse(mock_init.called)
Example #22
0
    def test_client_with_user_without_profile(self):
        """
        Verify client initialize successfully for users having no profile.
        """
        worker = User.objects.create_user(username='******', email='*****@*****.**')
        api_client = ecommerce_api_client(worker)

        self.assertEqual(api_client._store['session'].auth.__dict__['username'], worker.username)  # pylint: disable=protected-access
        self.assertIsNone(api_client._store['session'].auth.__dict__['full_name'])  # pylint: disable=protected-access
Example #23
0
def list_receipts(request):
    api = ecommerce_api_client(request.user)
    try:
        order_history = api.orders().get()
    except ConnectionError as e:
        logger.exception(e)
        order_history = []

    return render_to_response('payment/list_orders.html', {"order_history": order_history})
Example #24
0
    def test_client_with_user_without_profile(self):
        """
        Verify client initialize successfully for users having no profile.
        """
        worker = User.objects.create_user(username='******', email='*****@*****.**')
        api_client = ecommerce_api_client(worker)

        self.assertEqual(api_client._store['session'].auth.__dict__['username'], worker.username)  # pylint: disable=protected-access
        self.assertIsNone(api_client._store['session'].auth.__dict__['full_name'])  # pylint: disable=protected-access
Example #25
0
def refund_seat(course_enrollment, change_mode=False):
    """
    Attempt to initiate a refund for any orders associated with the seat being unenrolled,
    using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        change_mode (Boolean): change the course mode to free mode or not

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during communication with the E-Commerce Service.
        exceptions.Timeout: if the attempt to reach the commerce service timed out.
    """
    User = get_user_model()  # pylint:disable=invalid-name
    course_key_str = unicode(course_enrollment.course_id)
    enrollee = course_enrollment.user

    service_user = User.objects.get(
        username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info('Attempting to create a refund for user [%s], course [%s]...',
             enrollee.id, course_key_str)

    refund_ids = api_client.refunds.post({
        'course_id': course_key_str,
        'username': enrollee.username
    })

    if refund_ids:
        log.info('Refund successfully opened for user [%s], course [%s]: %r',
                 enrollee.id, course_key_str, refund_ids)

        _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            mode=course_enrollment.mode,
            user=enrollee,
        )
        if change_mode and CourseMode.can_auto_enroll(
                course_id=CourseKey.from_string(course_key_str)):
            course_enrollment.update_enrollment(
                mode=CourseMode.auto_enroll_mode(course_id=course_key_str),
                is_active=False,
                skip_refund=True)
            course_enrollment.save()
    else:
        log.info('No refund opened for user [%s], course [%s]', enrollee.id,
                 course_key_str)

    return refund_ids
Example #26
0
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    user_orders = []
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(
        user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(commerce_configuration,
                                            user,
                                            'orders',
                                            api=api,
                                            querystring=user_query,
                                            cache_key=cache_key)

    for order in commerce_user_orders:
        if order['status'].lower() == 'complete':
            date_placed = datetime.strptime(order['date_placed'],
                                            "%Y-%m-%dT%H:%M:%SZ")
            order_data = {
                'number':
                order['number'],
                'price':
                order['total_excl_tax'],
                'order_date':
                strftime_localized(date_placed, 'SHORT_DATE'),
                'receipt_url':
                EcommerceService().get_receipt_page_url(order['number']),
                'lines':
                order['lines'],
            }

            # If the order lines contain a product that is not a Seat
            # we do not want to display the Order Details button. It
            # will break the receipt page if used.
            for order_line in order['lines']:
                product = order_line.get('product')

                if product and product.get('product_class') != 'Seat':
                    order_data['receipt_url'] = ''
                    break
            user_orders.append(order_data)

    return user_orders
Example #27
0
def get_credit_provider_attribute_values(course_key, attribute_name):
    """Get the course information from ecommerce and parse the data to get providers.

    Arguments:
        course_key (CourseKey): The identifier for the course.
        attribute_name (String): Name of the attribute of credit provider.

    Returns:
        List of provided credit provider attribute values.
    """
    course_id = six.text_type(course_key)
    credit_config = CreditConfig.current()

    cache_key = None
    attribute_values = None

    if credit_config.is_cache_enabled:
        cache_key = '{key_prefix}.{course_key}.{attribute_name}'.format(
            key_prefix=credit_config.CACHE_KEY,
            course_key=course_id,
            attribute_name=attribute_name
        )
        attribute_values = cache.get(cache_key)

    if attribute_values is not None:
        return attribute_values

    try:
        user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
        response = ecommerce_api_client(user).courses(course_id).get(include_products=1)
    except Exception:  # pylint: disable=broad-except
        log.exception(u"Failed to receive data from the ecommerce course API for Course ID '%s'.", course_id)
        return attribute_values

    if not response:
        log.info(u"No Course information found from ecommerce API for Course ID '%s'.", course_id)
        return attribute_values

    provider_ids = []
    for product in response.get('products'):
        provider_ids += [
            attr.get('value') for attr in product.get('attribute_values') if attr.get('name') == 'credit_provider'
        ]

    attribute_values = []
    credit_providers = CreditProvider.get_credit_providers()
    for provider in credit_providers:
        if provider['id'] in provider_ids:
            attribute_values.append(provider[attribute_name])

    if credit_config.is_cache_enabled:
        cache.set(cache_key, attribute_values, credit_config.cache_ttl)

    return attribute_values
Example #28
0
    def _collect_one_click_purchase_eligibility_data(self):
        """
        Extend the program data with data about learner's eligibility for one click purchase,
        discount data of the program and SKUs of seats that should be added to basket.
        """
        applicable_seat_types = self.data['applicable_seat_types']
        is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
        skus = []
        if is_learner_eligible_for_one_click_purchase:
            for course in self.data['courses']:
                is_learner_eligible_for_one_click_purchase = not any(
                    course_run['is_enrolled'] for course_run in course['course_runs']
                )
                if is_learner_eligible_for_one_click_purchase:
                    published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
                    if len(published_course_runs) == 1:
                        for seat in published_course_runs[0]['seats']:
                            if seat['type'] in applicable_seat_types and seat['sku']:
                                skus.append(seat['sku'])
                    else:
                        # If a course in the program has more than 1 published course run
                        # learner won't be eligible for a one click purchase.
                        is_learner_eligible_for_one_click_purchase = False
                        skus = []
                        break
                else:
                    skus = []
                    break

        if skus:
            try:
                User = get_user_model()
                service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
                api = ecommerce_api_client(service_user)

                # Make an API call to calculate the discounted price
                discount_data = api.baskets.calculate.get(sku=skus)

                program_discounted_price = discount_data['total_incl_tax']
                program_full_price = discount_data['total_incl_tax_excl_discounts']
                discount_data['is_discounted'] = program_discounted_price < program_full_price
                discount_data['discount_value'] = program_full_price - program_discounted_price

                self.data.update({
                    'discount_data': discount_data,
                    'full_program_price': discount_data['total_incl_tax']
                })
            except (ConnectionError, SlumberBaseException, Timeout):
                log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus))

        self.data.update({
            'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
            'skus': skus,
        })
Example #29
0
def get_course_final_price(user, sku, course_price):
    """
    Return the course's discounted price for a user if user is eligible for any otherwise return course original price.
    """
    price_details = {}
    try:
        price_details = ecommerce_api_client(user).baskets.calculate.get(
            sku=[sku],
            username=user.username,
        )
    except (SlumberBaseException, ConnectionError, Timeout) as exc:     # pylint: disable=unused-variable
        pass
    return price_details.get('total_incl_tax', course_price)
Example #30
0
 def get(self, request, number):
     """ HTTP handler. """
     # If the account activation requirement is disabled for this installation, override the
     # anonymous user object attached to the request with the actual user object (if it exists)
     if not request.user.is_authenticated() and is_account_activation_requirement_disabled():
         try:
             request.user = User.objects.get(id=request.session._session_cache['_auth_user_id'])
         except User.DoesNotExist:
             return JsonResponse(status=403)
     try:
         order = ecommerce_api_client(request.user).orders(number).get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
 def get(self, request, number):
     """ HTTP handler. """
     # If the account activation requirement is disabled for this installation, override the
     # anonymous user object attached to the request with the actual user object (if it exists)
     if not request.user.is_authenticated and is_account_activation_requirement_disabled():
         try:
             request.user = User.objects.get(id=request.session._session_cache['_auth_user_id'])  # lint-amnesty, pylint: disable=protected-access
         except User.DoesNotExist:
             return JsonResponse(status=403)
     try:
         order = ecommerce_api_client(request.user).orders(number).get()
         return JsonResponse(order)
     except exceptions.HttpNotFoundError:
         return JsonResponse(status=404)
Example #32
0
def get_credit_provider_display_names(course_key):
    """Get the course information from ecommerce and parse the data to get providers.

    Arguments:
        course_key (CourseKey): The identifier for the course.

    Returns:
        List of credit provider display names.
    """
    course_id = unicode(course_key)
    credit_config = CreditConfig.current()

    cache_key = None
    provider_names = None

    if credit_config.is_cache_enabled:
        cache_key = "{key_prefix}.{course_key}".format(key_prefix=credit_config.CACHE_KEY, course_key=course_id)
        provider_names = cache.get(cache_key)

    if provider_names is not None:
        return provider_names

    try:
        user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
        response = ecommerce_api_client(user).courses(course_id).get(include_products=1)
    except Exception:  # pylint: disable=broad-except
        log.exception("Failed to receive data from the ecommerce course API for Course ID '%s'.", course_id)
        return provider_names

    if not response:
        log.info("No Course information found from ecommerce API for Course ID '%s'.", course_id)
        return provider_names

    provider_ids = []
    for product in response.get("products"):
        provider_ids += [
            attr.get("value") for attr in product.get("attribute_values") if attr.get("name") == "credit_provider"
        ]

    provider_names = []
    credit_providers = CreditProvider.get_credit_providers()
    for provider in credit_providers:
        if provider["id"] in provider_ids:
            provider_names.append(provider["display_name"])

    if credit_config.is_cache_enabled:
        cache.set(cache_key, provider_names, credit_config.cache_ttl)

    return provider_names
Example #33
0
    def _collect_one_click_purchase_eligibility_data(self):
        """
        Extend the program data with data about learner's eligibility for one click purchase,
        discount data of the program and SKUs of seats that should be added to basket.
        """
        applicable_seat_types = self.data['applicable_seat_types']
        is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
        skus = []
        if is_learner_eligible_for_one_click_purchase:
            for course in self.data['courses']:
                is_learner_eligible_for_one_click_purchase = not any(
                    course_run['is_enrolled'] for course_run in course['course_runs']
                )
                if is_learner_eligible_for_one_click_purchase:
                    published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
                    if len(published_course_runs) == 1:
                        for seat in published_course_runs[0]['seats']:
                            if seat['type'] in applicable_seat_types:
                                skus.append(seat['sku'])
                    else:
                        # If a course in the program has more than 1 published course run
                        # learner won't be eligible for a one click purchase.
                        is_learner_eligible_for_one_click_purchase = False
                        skus = []
                        break
                else:
                    skus = []
                    break

        if skus:
            try:
                User = get_user_model()
                service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
                api = ecommerce_api_client(service_user)

                # Make an API call to calculate the discounted price
                discount_data = api.baskets.calculate.get(sku=skus)

                self.data.update({
                    'discount_data': discount_data,
                    'full_program_price': discount_data['total_incl_tax']
                })
            except (ConnectionError, SlumberBaseException, Timeout):
                log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus))

        self.data.update({
            'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
            'skus': skus,
        })
Example #34
0
    def test_tracking_context(self):
        """
        Ensure the tracking context is set up in the api client correctly and
        automatically.
        """

        # fake an ecommerce api request.
        httpretty.register_uri(
            httpretty.POST,
            '{}/baskets/1/'.format(TEST_API_URL),
            status=200, body='{}',
            adding_headers={'Content-Type': JSON}
        )

        mock_tracker = mock.Mock()
        mock_tracker.resolve_context = mock.Mock(return_value={'client_id': self.TEST_CLIENT_ID, 'ip': '127.0.0.1'})
        with mock.patch('openedx.core.djangoapps.commerce.utils.tracker.get_tracker', return_value=mock_tracker):
            ecommerce_api_client(self.user).baskets(1).post()

        # make sure the request's JWT token payload included correct tracking context values.
        actual_header = httpretty.last_request().headers['Authorization']
        expected_payload = {
            'username': self.user.username,
            'full_name': self.user.profile.name,
            'email': self.user.email,
            'iss': settings.JWT_AUTH['JWT_ISSUER'],
            'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=settings.JWT_AUTH['JWT_EXPIRATION']),
            'tracking_context': {
                'lms_user_id': self.user.id,  # pylint: disable=no-member
                'lms_client_id': self.TEST_CLIENT_ID,
                'lms_ip': '127.0.0.1',
            },
        }

        expected_header = 'JWT {}'.format(jwt.encode(expected_payload, TEST_API_SIGNING_KEY))
        self.assertEqual(actual_header, expected_header)
Example #35
0
    def __init__(self, user):
        """
        Create an E-Commerce API client, authenticated with the API token from Django settings.

        This method retrieves an authenticated API client that can be used
        to access the ecommerce API. It raises an exception to be caught at
        a higher level if the package doesn't have OpenEdX resources available.
        """
        if ecommerce_api_client is None:
            raise NotConnectedToOpenEdX(
                _('To get a ecommerce_api_client, this package must be '
                  'installed in an Open edX environment.'))

        self.user = user
        self.client = ecommerce_api_client(user)
Example #36
0
def get_pricing_data(skus):
    """
    Get the pricing data from ecommerce for given skus.
    """
    user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api = ecommerce_api_client(user)
    pricing_data = api.baskets.calculate.get(sku=skus, is_anonymous=True)
    discount_value = float(pricing_data['total_incl_tax_excl_discounts']) - float(pricing_data['total_incl_tax'])
    ecommerce_service = EcommerceService()
    purchase_url = ecommerce_service.get_checkout_page_url(*skus)
    pricing_data.update({
        'is_discounted': pricing_data['total_incl_tax'] != pricing_data['total_incl_tax_excl_discounts'],
        'discount_value': discount_value,
        'purchase_url': purchase_url,
    })
    return pricing_data
Example #37
0
    def test_client_unicode(self):
        """
        The client should handle json responses properly when they contain
        unicode character data.

        Regression test for ECOM-1606.
        """
        expected_content = '{"result": "Préparatoire"}'
        httpretty.register_uri(
            httpretty.GET,
            '{}/baskets/1/order/'.format(settings.ECOMMERCE_API_URL.strip('/')),
            status=200, body=expected_content,
            adding_headers={'Content-Type': JSON},
        )
        actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
        self.assertEqual(actual_object, {u"result": u"Préparatoire"})
Example #38
0
    def test_client_unicode(self):
        """
        The client should handle json responses properly when they contain
        unicode character data.

        Regression test for ECOM-1606.
        """
        expected_content = '{"result": "Préparatoire"}'
        httpretty.register_uri(
            httpretty.GET,
            '{}/baskets/1/order/'.format(settings.ECOMMERCE_API_URL.strip('/')),
            status=200, body=expected_content,
            adding_headers={'Content-Type': JSON},
        )
        actual_object = ecommerce_api_client(self.user).baskets(1).order.get()
        self.assertEqual(actual_object, {u"result": u"Préparatoire"})
Example #39
0
 def get_final_price(self, mode, request):
     """
     Get course mode's SKU discounted price after applying any entitlement available for this user.
     """
     try:
         client = ecommerce_api_client(request.user)
         endpoint = client.baskets.calculate
         price_details = endpoint.get(sku=[mode['sku']])
         price = price_details['total_incl_tax']
         if price != mode['min_price']:
             return '${}'.format(price)
     except HttpClientError:
         logger.error(
             "Failed to get price details for course mode's SKU '{sku}' for username '{username}'"
             .format(sku=mode['sku'], username=request.user.username))
     return mode['original_price']
Example #40
0
def sponsored_user(user, course_id):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    commerce_configuration = CommerceConfiguration.current()
    user_query = {"username": user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = (commerce_configuration.CACHE_KEY + "." +
                 str(user.id) if use_cache else None)
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration,
        "orders",
        api=api,
        querystring=user_query,
        cache_key=cache_key,
    )
    user_order = "n/a"
    if commerce_user_orders:
        if not user.is_staff:
            for order in commerce_user_orders:
                order_course_id = order["lines"][0]["product"][
                    "attribute_values"][1]["value"]
                if not order["vouchers"]:
                    order_coupon_code = "nocode"
                else:
                    order_coupon_code = order["vouchers"][0]["code"]
                try:
                    Sponsored_course = Sponsored_course_users.objects.get(
                        course_id=course_id,
                        coupon_code__contains=order_coupon_code)
                    user_order = Sponsored_course.video_url
                except ObjectDoesNotExist:
                    user_order = "n/a"
        else:
            user_order = "n/a"
    else:
        user_order = "n/a"
    return user_order
Example #41
0
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Arguments:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    user_orders = []
    allowed_course_modes = ['professional', 'verified', 'credit']
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration, user, 'orders', api=api, querystring=user_query, cache_key=cache_key
    )

    for order in commerce_user_orders:
        if order['status'].lower() == 'complete':
            for line in order['lines']:
                product = line.get('product')
                if product:
                    for attribute in product['attribute_values']:
                        if attribute['name'] == 'certificate_type' and attribute['value'] in allowed_course_modes:
                            try:
                                date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
                                order_data = {
                                    'number': order['number'],
                                    'price': order['total_excl_tax'],
                                    'title': order['lines'][0]['title'],
                                    'order_date': strftime_localized(
                                        date_placed.replace(tzinfo=pytz.UTC), 'SHORT_DATE'
                                    ),
                                    'receipt_url': commerce_configuration.receipt_page + order['number']
                                }
                                user_orders.append(order_data)
                            except KeyError:
                                log.exception('Invalid order structure: %r', order)
                                return no_data

    return user_orders
Example #42
0
def get_user_orders(user):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Arguments:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    user_orders = []
    allowed_course_modes = ["professional", "verified", "credit"]
    commerce_configuration = CommerceConfiguration.current()
    user_query = {"username": user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + "." + str(user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration, user, "orders", api=api, querystring=user_query, cache_key=cache_key
    )

    for order in commerce_user_orders:
        if order["status"].lower() == "complete":
            for line in order["lines"]:
                product = line.get("product")
                if product:
                    for attribute in product["attribute_values"]:
                        if attribute["name"] == "certificate_type" and attribute["value"] in allowed_course_modes:
                            try:
                                date_placed = datetime.strptime(order["date_placed"], "%Y-%m-%dT%H:%M:%SZ")
                                order_data = {
                                    "number": order["number"],
                                    "price": order["total_excl_tax"],
                                    "title": order["lines"][0]["title"],
                                    "order_date": strftime_localized(
                                        date_placed.replace(tzinfo=pytz.UTC), "SHORT_DATE"
                                    ),
                                    "receipt_url": commerce_configuration.receipt_page + order["number"],
                                }
                                user_orders.append(order_data)
                            except KeyError:
                                log.exception("Invalid order structure: %r", order)
                                return no_data

    return user_orders
Example #43
0
def refund_seat(course_enrollment, request_user):
    """
    Attempt to initiate a refund for any orders associated with the seat being
    unenrolled, using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        request_user: the user as whom to authenticate to the commerce service
            when attempting to initiate the refund.

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during
            communication with the commerce service.
        exceptions.Timeout: if the attempt to reach the commerce service timed
            out.

    """
    course_key_str = unicode(course_enrollment.course_id)
    unenrolled_user = course_enrollment.user

    try:
        refund_ids = ecommerce_api_client(request_user
                                          or unenrolled_user).refunds.post({
                                              'course_id':
                                              course_key_str,
                                              'username':
                                              unenrolled_user.username
                                          })
    except HttpClientError, exc:
        if exc.response.status_code == 403 and request_user != unenrolled_user:
            # this is a known limitation; commerce service does not presently
            # support the case of a non-superusers initiating a refund on
            # behalf of another user.
            log.warning(
                "User [%s] was not authorized to initiate a refund for user [%s] "
                "upon unenrollment from course [%s]", request_user.id,
                unenrolled_user.id, course_key_str)
            return []
        else:
            # no other error is anticipated, so re-raise the Exception
            raise exc
Example #44
0
def refund_entitlement(course_entitlement):
    """
    Attempt a refund of a course entitlement
    :param course_entitlement:
    :return:
    """
    user_model = get_user_model()
    enrollee = course_entitlement.user
    entitlement_uuid = str(course_entitlement.uuid)

    service_user = user_model.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info(
        'Attempting to create a refund for user [%s], course entitlement [%s]...',
        enrollee.username,
        entitlement_uuid
    )

    refund_ids = api_client.refunds.post(
        {
            'order_number': course_entitlement.order_number,
            'username': enrollee.username,
            'entitlement_uuid': entitlement_uuid,
        }
    )

    if refund_ids:
        log.info(
            'Refund successfully opened for user [%s], course entitlement [%s]: %r',
            enrollee.username,
            entitlement_uuid,
            refund_ids,
        )

        _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            course_product=course_entitlement,
            is_entitlement=True
        )
    else:
        log.info('No refund opened for user [%s], course entitlement [%s]', enrollee.id, entitlement_uuid)

    return refund_ids
Example #45
0
def refund_seat(course_enrollment, change_mode=False):
    """
    Attempt to initiate a refund for any orders associated with the seat being unenrolled,
    using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        change_mode (Boolean): change the course mode to free mode or not

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during communication with the E-Commerce Service.
        exceptions.Timeout: if the attempt to reach the commerce service timed out.
    """
    User = get_user_model()  # pylint:disable=invalid-name
    course_key_str = unicode(course_enrollment.course_id)
    enrollee = course_enrollment.user

    service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str)

    refund_ids = api_client.refunds.post({'course_id': course_key_str, 'username': enrollee.username})

    if refund_ids:
        log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids)

        _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            mode=course_enrollment.mode,
            user=enrollee,
        )
        if change_mode and CourseMode.can_auto_enroll(course_id=CourseKey.from_string(course_key_str)):
            course_enrollment.update_enrollment(mode=CourseMode.auto_enroll_mode(course_id=course_key_str),
                                                is_active=False, skip_refund=True)
            course_enrollment.save()
    else:
        log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)

    return refund_ids
Example #46
0
def sponsored_user(user,course_id):
    """Given a user, get the detail of all the orders from the Ecommerce service.

    Args:
        user (User): The user to authenticate as when requesting ecommerce.

    Returns:
        list of dict, representing orders returned by the Ecommerce service.
    """
    no_data = []
    commerce_configuration = CommerceConfiguration.current()
    user_query = {'username': user.username}

    use_cache = commerce_configuration.is_cache_enabled
    cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
    api = ecommerce_api_client(user)
    commerce_user_orders = get_edx_api_data(
        commerce_configuration, 'orders', api=api, querystring=user_query, cache_key=cache_key
    )
    user_order = 0
    #log.info('======data %s', commerce_user_orders)
    if not user.is_staff :
        for order in commerce_user_orders:
            order_course_id = order['lines'][0]['product']['attribute_values'][1]['value']
            if not order['vouchers'] :
                order_coupon_code = "nocode"        
            else :
                order_coupon_code = order['vouchers'][0]['code']
                #log.info('======coupon_code %s', order_coupon_code)
            try:
                Sponsored_course = Sponsored_course_users.objects.filter(course_id=course_id,coupon_code__contains=order_coupon_code).values()
                if Sponsored_course:
                    user_order = Sponsored_course
                # log.info('======sponsored_data %s', Sponsored_course.query)
                # log.info('======lm_course_id %s', order_course_id)
                # log.info('======coupon_code %s', order_coupon_code)
                # log.info('======user_order %s', user_order)
            except:
                user_order = 0
                #log.info('======user_order %s', user_order)
        return user_order
Example #47
0
def create_site_on_ecommerce(ecommerce_worker, lms_site_url, ecommerce_site_url, client, validated_data):
    """
    Creates sites, themes and configurations on ecommerce.
    """
    # TODO: EdxRestApiClient is depricated, use OAuthAPIClient instead
    ecommerce_client = ecommerce_api_client(ecommerce_worker)

    request_data = {
        'lms_site_url': lms_site_url,
        'ecommerce_site_domain': urlparse(ecommerce_site_url).netloc,
        'site_theme': '{}-ecommerce'.format(validated_data['site_theme']),
        'oauth_settings': {
            'SOCIAL_AUTH_EDX_OIDC_KEY': client.client_id,
            'SOCIAL_AUTH_EDX_OIDC_SECRET': client.client_secret,
            'SOCIAL_AUTH_EDX_OIDC_ID_TOKEN_DECRYPTION_KEY': client.client_secret,
            'SOCIAL_AUTH_EDX_OIDC_ISSUERS': [lms_site_url],
            'SOCIAL_AUTH_EDX_OIDC_PUBLIC_URL_ROOT': '{}/oauth2'.format(lms_site_url),
            'SOCIAL_AUTH_EDX_OIDC_URL_ROOT': '{}/oauth2'.format(lms_site_url),
        }
    }
    optional_fields = [
        'client_side_payment_processor',
        'ecommerce_from_email',
        'payment_processors',
        'payment_support_email',
        'site_partner',
    ]
    for field in optional_fields:
        if validated_data.get(field):
            request_data[field] = validated_data.get(field)

    LOGGER.info('Sending the following data to ecommerce for site creation: {}'.format(request_data))

    try:
        response = ecommerce_client.resource('/colaraz/api/v1/site-org/').post(request_data)
    except HttpClientError as ex:
        LOGGER.error('Error while sending data to ecommerce: {}'.format(str(ex.content)))
        raise rest_serializers.ValidationError(str(ex.content))

    return response
Example #48
0
def refund_seat(course_enrollment, request_user):
    """
    Attempt to initiate a refund for any orders associated with the seat being
    unenrolled, using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment
        request_user: the user as whom to authenticate to the commerce service
            when attempting to initiate the refund.

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during
            communication with the commerce service.
        exceptions.Timeout: if the attempt to reach the commerce service timed
            out.

    """
    course_key_str = unicode(course_enrollment.course_id)
    unenrolled_user = course_enrollment.user

    try:
        refund_ids = ecommerce_api_client(request_user or unenrolled_user).refunds.post(
            {'course_id': course_key_str, 'username': unenrolled_user.username}
        )
    except HttpClientError, exc:
        if exc.response.status_code == 403 and request_user != unenrolled_user:
            # this is a known limitation; commerce service does not presently
            # support the case of a non-superusers initiating a refund on
            # behalf of another user.
            log.warning("User [%s] was not authorized to initiate a refund for user [%s] "
                        "upon unenrollment from course [%s]", request_user.id, unenrolled_user.id, course_key_str)
            return []
        else:
            # no other error is anticipated, so re-raise the Exception
            raise exc
Example #49
0
def get_pricing_data(skus):
    """
    Get the pricing data from ecommerce for given skus.
    """
    user = User.objects.get(
        username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api = ecommerce_api_client(user)
    pricing_data = api.baskets.calculate.get(sku=skus, is_anonymous=True)
    discount_value = float(
        pricing_data['total_incl_tax_excl_discounts']) - float(
            pricing_data['total_incl_tax'])
    ecommerce_service = EcommerceService()
    purchase_url = ecommerce_service.get_checkout_page_url(*skus)
    pricing_data.update({
        'is_discounted':
        pricing_data['total_incl_tax'] !=
        pricing_data['total_incl_tax_excl_discounts'],
        'discount_value':
        discount_value,
        'purchase_url':
        purchase_url,
    })
    return pricing_data
Example #50
0
def refund_order_voucher(sender,
                         course_enrollment=None,
                         skip_refund=False,
                         **kwargs):  # pylint: disable=unused-argument
    """
        Call the /api/v2/enterprise/coupons/create_refunded_voucher/ API to create new voucher and assign it to user.
    """

    if skip_refund:
        return
    if not course_enrollment.refundable():
        return
    if not EnterpriseCourseEnrollment.objects.filter(
            enterprise_customer_user__user_id=course_enrollment.user_id,
            course_id=str(course_enrollment.course.id)).exists():
        return

    service_user = User.objects.get(
        username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    client = ecommerce_api_client(service_user)
    order_number = course_enrollment.get_order_attribute_value('order_number')
    if order_number:
        client.enterprise.coupons.create_refunded_voucher.post(
            {"order": order_number})
Example #51
0
def get_basket(user, order_id):
    return ecommerce_api_client(user).baskets(order_id).get()
Example #52
0
    def get(
        self, request, course_id,
        always_show_payment=False,
        current_step=None,
        message=FIRST_TIME_VERIFY_MSG
    ):
        """
        Render the payment and verification flow.

        Arguments:
            request (HttpRequest): The request object.
            course_id (unicode): The ID of the course the user is trying
                to enroll in.

        Keyword Arguments:
            always_show_payment (bool): If True, show the payment steps
                even if the user has already paid.  This is useful
                for users returning to the flow after paying.
            current_step (string): The current step in the flow.
            message (string): The messaging to display.

        Returns:
            HttpResponse

        Raises:
            Http404: The course does not exist or does not
                have a verified mode.

        """
        # Parse the course key
        # The URL regex should guarantee that the key format is valid.
        course_key = CourseKey.from_string(course_id)
        course = modulestore().get_course(course_key)

        # Verify that the course exists
        if course is None:
            log.warn(u"Could not find course with ID %s.", course_id)
            raise Http404

        # Check whether the user has access to this course
        # based on country access rules.
        redirect_url = embargo_api.redirect_if_blocked(
            course_key,
            user=request.user,
            ip_address=get_ip(request),
            url=request.path
        )
        if redirect_url:
            return redirect(redirect_url)

        # If the verification deadline has passed
        # then show the user a message that he/she can't verify.
        #
        # We're making the assumptions (enforced in Django admin) that:
        #
        # 1) Only verified modes have verification deadlines.
        #
        # 2) If set, verification deadlines are always AFTER upgrade deadlines, because why would you
        #   let someone upgrade into a verified track if they can't complete verification?
        #
        verification_deadline = VerificationDeadline.deadline_for_course(course.id)
        response = self._response_if_deadline_passed(course, self.VERIFICATION_DEADLINE, verification_deadline)
        if response is not None:
            log.info(u"Verification deadline for '%s' has passed.", course.id)
            return response

        # Retrieve the relevant course mode for the payment/verification flow.
        #
        # WARNING: this is technical debt!  A much better way to do this would be to
        # separate out the payment flow and use the product SKU to figure out what
        # the user is trying to purchase.
        #
        # Nonetheless, for the time being we continue to make the really ugly assumption
        # that at some point there was a paid course mode we can query for the price.
        relevant_course_mode = self._get_paid_mode(course_key)

        # If we can find a relevant course mode, then log that we're entering the flow
        # Otherwise, this course does not support payment/verification, so respond with a 404.
        if relevant_course_mode is not None:
            if CourseMode.is_verified_mode(relevant_course_mode):
                log.info(
                    u"Entering payment and verification flow for user '%s', course '%s', with current step '%s'.",
                    request.user.id, course_id, current_step
                )
            else:
                log.info(
                    u"Entering payment flow for user '%s', course '%s', with current step '%s'",
                    request.user.id, course_id, current_step
                )
        else:
            # Otherwise, there has never been a verified/paid mode,
            # so return a page not found response.
            log.warn(
                u"No paid/verified course mode found for course '%s' for verification/payment flow request",
                course_id
            )
            raise Http404

        # If the user is trying to *pay* and the upgrade deadline has passed,
        # then they shouldn't be able to enter the flow.
        #
        # NOTE: This should match the availability dates used by the E-Commerce service
        # to determine whether a user can purchase a product.  The idea is that if the service
        # won't fulfill the order, we shouldn't even let the user get into the payment flow.
        #
        user_is_trying_to_pay = message in [self.FIRST_TIME_VERIFY_MSG, self.UPGRADE_MSG]
        if user_is_trying_to_pay:
            upgrade_deadline = relevant_course_mode.expiration_datetime
            response = self._response_if_deadline_passed(course, self.UPGRADE_DEADLINE, upgrade_deadline)
            if response is not None:
                log.info(u"Upgrade deadline for '%s' has passed.", course.id)
                return response

        # Check whether the user has verified, paid, and enrolled.
        # A user is considered "paid" if he or she has an enrollment
        # with a paid course mode (such as "verified").
        # For this reason, every paid user is enrolled, but not
        # every enrolled user is paid.
        # If the course mode is not verified(i.e only paid) then already_verified is always True
        already_verified = (
            self._check_already_verified(request.user)
            if CourseMode.is_verified_mode(relevant_course_mode)
            else True
        )
        already_paid, is_enrolled = self._check_enrollment(request.user, course_key)

        # Redirect the user to a more appropriate page if the
        # messaging won't make sense based on the user's
        # enrollment / payment / verification status.
        sku_to_use = relevant_course_mode.sku
        purchase_workflow = request.GET.get('purchase_workflow', 'single')
        if purchase_workflow == 'bulk' and relevant_course_mode.bulk_sku:
            sku_to_use = relevant_course_mode.bulk_sku
        redirect_response = self._redirect_if_necessary(
            message,
            already_verified,
            already_paid,
            is_enrolled,
            course_key,
            user_is_trying_to_pay,
            request.user,
            sku_to_use
        )
        if redirect_response is not None:
            return redirect_response

        display_steps = self._display_steps(
            always_show_payment,
            already_verified,
            already_paid,
            relevant_course_mode
        )

        # Override the actual value if account activation has been disabled
        # Also see the reference to this parameter in context dictionary further down
        user_is_active = self._get_user_active_status(request.user)
        requirements = self._requirements(display_steps, user_is_active)

        if current_step is None:
            current_step = display_steps[0]['name']

        # Allow the caller to skip the first page
        # This is useful if we want the user to be able to
        # use the "back" button to return to the previous step.
        # This parameter should only work for known skip-able steps
        if request.GET.get('skip-first-step') and current_step in self.SKIP_STEPS:
            display_step_names = [step['name'] for step in display_steps]
            current_step_idx = display_step_names.index(current_step)
            if (current_step_idx + 1) < len(display_steps):
                current_step = display_steps[current_step_idx + 1]['name']

        courseware_url = ""
        if not course.start or course.start < datetime.datetime.today().replace(tzinfo=UTC):
            courseware_url = reverse(
                'course_root',
                kwargs={'course_id': unicode(course_key)}
            )

        full_name = (
            request.user.profile.name
            if request.user.profile.name
            else ""
        )

        # If the user set a contribution amount on another page,
        # use that amount to pre-fill the price selection form.
        contribution_amount = request.session.get(
            'donation_for_course', {}
        ).get(unicode(course_key), '')

        # Remember whether the user is upgrading
        # so we can fire an analytics event upon payment.
        request.session['attempting_upgrade'] = (message == self.UPGRADE_MSG)

        # Determine the photo verification status
        verification_good_until = self._verification_valid_until(request.user)

        # get available payment processors
        if relevant_course_mode.sku:
            # transaction will be conducted via ecommerce service
            processors = ecommerce_api_client(request.user).payment.processors.get()
        else:
            # transaction will be conducted using legacy shopping cart
            processors = [settings.CC_PROCESSOR_NAME]

        # Render the top-level page
        context = {
            'contribution_amount': contribution_amount,
            'course': course,
            'course_key': unicode(course_key),
            'checkpoint_location': request.GET.get('checkpoint'),
            'course_mode': relevant_course_mode,
            'courseware_url': courseware_url,
            'current_step': current_step,
            'disable_courseware_js': True,
            'display_steps': display_steps,
            'is_active': json.dumps(user_is_active),
            'user_email': request.user.email,
            'message_key': message,
            'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
            'processors': processors,
            'requirements': requirements,
            'user_full_name': full_name,
            'verification_deadline': verification_deadline or "",
            'already_verified': already_verified,
            'verification_good_until': verification_good_until,
            'capture_sound': staticfiles_storage.url("audio/camera_capture.wav"),
            'nav_hidden': True,
            'is_ab_testing': 'begin-flow' in request.path,
        }

        return render_to_response("verify_student/pay_and_verify.html", context)
Example #53
0
    def _collect_one_click_purchase_eligibility_data(self):
        """
        Extend the program data with data about learner's eligibility for one click purchase,
        discount data of the program and SKUs of seats that should be added to basket.
        """
        applicable_seat_types = self.data['applicable_seat_types']
        is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
        skus = []
        bundle_variant = 'full'
        if is_learner_eligible_for_one_click_purchase:
            for course in self.data['courses']:
                add_course_sku = False
                published_course_runs = filter(lambda run: run['status'] == 'published', course['course_runs'])
                if len(published_course_runs) == 1:
                    # Look at the course runs for a course and determine if the course SKU should be added.
                    course_run = published_course_runs[0]
                    (enrollment_mode, active) = CourseEnrollment.enrollment_mode_for_user(
                        self.user,
                        CourseKey.from_string(course_run['key'])
                    )

                    if enrollment_mode is not None and active is not None:
                        # Check all the applicable seat types
                        # this will also check for no-id-professional as professional
                        applicable_seat = any(seat_type in enrollment_mode for seat_type in applicable_seat_types)

                        # If no applicable seat is found add the course SKU to the list
                        if not applicable_seat or not active:
                            add_course_sku = True
                    else:
                        # There is no enrollment information for the course add the course SKU
                        add_course_sku = True

                    if add_course_sku:
                        for seat in published_course_runs[0]['seats']:
                            if seat['type'] in applicable_seat_types and seat['sku']:
                                skus.append(seat['sku'])
                    else:
                        bundle_variant = 'partial'
                else:
                    # If a course in the program has more than 1 published course run
                    # learner won't be eligible for a one click purchase.
                    is_learner_eligible_for_one_click_purchase = False
                    skus = []
                    break

        if skus:
            try:
                api_user = self.user
                if not self.user.is_authenticated():
                    user = get_user_model()
                    service_user = user.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
                    api_user = service_user

                api = ecommerce_api_client(api_user)

                # Make an API call to calculate the discounted price
                discount_data = api.baskets.calculate.get(sku=skus)

                program_discounted_price = discount_data['total_incl_tax']
                program_full_price = discount_data['total_incl_tax_excl_discounts']
                discount_data['is_discounted'] = program_discounted_price < program_full_price
                discount_data['discount_value'] = program_full_price - program_discounted_price

                self.data.update({
                    'discount_data': discount_data,
                    'full_program_price': discount_data['total_incl_tax'],
                    'variant': bundle_variant
                })
            except (ConnectionError, SlumberBaseException, Timeout):
                log.exception('Failed to get discount price for following product SKUs: %s ', ', '.join(skus))
                self.data.update({
                    'discount_data': {'is_discounted': False}
                })
        else:
            is_learner_eligible_for_one_click_purchase = False

        self.data.update({
            'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
            'skus': skus,
        })
Example #54
0
    def post(self, request, *args, **kwargs):
        """
        Attempt to create the basket and enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        embargo_response = embargo_api.get_embargo_response(request, course_key, user)

        if embargo_response:
            return embargo_response

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # If there is no honor course mode, this most likely a Prof-Ed course. Return an error so that the JS
        # redirects to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)

        if not honor_mode:
            msg = Messages.NO_HONOR_MODE.format(course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
        elif not honor_mode.sku:
            # If there are no course modes with SKUs, enroll the user without contacting the external API.
            msg = Messages.NO_SKU_ENROLLED.format(enrollment_mode=CourseMode.HONOR, course_id=course_id,
                                                  username=user.username)
            log.info(msg)
            self._enroll(course_key, user)
            self._handle_marketing_opt_in(request, course_key, user)
            return DetailResponse(msg)

        # Setup the API

        try:
            api = ecommerce_api_client(user)
        except ValueError:
            self._enroll(course_key, user)
            msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
            log.debug(msg)
            return DetailResponse(msg)

        response = None

        # Make the API call
        try:
            response_data = api.baskets.post({
                'products': [{'sku': honor_mode.sku}],
                'checkout': True,
            })

            payment_data = response_data["payment_data"]
            if payment_data:
                # Pass data to the client to begin the payment flow.
                response = JsonResponse(payment_data)
            elif response_data['order']:
                # The order was completed immediately because there is no charge.
                msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
                log.debug(msg)
                response = DetailResponse(msg)
            else:
                msg = u'Unexpected response from basket endpoint.'
                log.error(
                    msg + u' Could not enroll user %(username)s in course %(course_id)s.',
                    {'username': user.id, 'course_id': course_id},
                )
                raise InvalidResponseError(msg)
        except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
            log.exception(ex.message)
            return InternalRequestErrorResponse(ex.message)
        finally:
            audit_log(
                'checkout_requested',
                course_id=course_id,
                mode=honor_mode.slug,
                processor_name=None,
                user_id=user.id
            )

        self._handle_marketing_opt_in(request, course_key, user)
        return response
Example #55
0
def refund_entitlement(course_entitlement):
    """
    Attempt a refund of a course entitlement. Verify the User before calling this refund method

    Returns:
        bool: True if the Refund is successfully processed.
    """
    user_model = get_user_model()
    enrollee = course_entitlement.user
    entitlement_uuid = str(course_entitlement.uuid)

    if not is_commerce_service_configured():
        log.error(
            'Ecommerce service is not configured, cannot refund for user [%s], course entitlement [%s].',
            enrollee.id,
            entitlement_uuid
        )
        return False

    service_user = user_model.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info(
        'Attempting to create a refund for user [%s], course entitlement [%s]...',
        enrollee.id,
        entitlement_uuid
    )

    try:
        refund_ids = api_client.refunds.post(
            {
                'order_number': course_entitlement.order_number,
                'username': enrollee.username,
                'entitlement_uuid': entitlement_uuid,
            }
        )
    except Exception as exc:  # pylint: disable=broad-except
        # Catch any possible exceptions from the Ecommerce service to ensure we fail gracefully
        log.exception(
            "Unexpected exception while attempting to initiate refund for user [%s], "
            "course entitlement [%s] message: [%s]",
            enrollee.id,
            course_entitlement.uuid,
            str(exc)
        )
        return False

    if refund_ids:
        log.info(
            'Refund successfully opened for user [%s], course entitlement [%s]: %r',
            enrollee.id,
            entitlement_uuid,
            refund_ids,
        )

        return _process_refund(
            refund_ids=refund_ids,
            api_client=api_client,
            mode=course_entitlement.mode,
            user=enrollee,
            always_notify=True,
        )
    else:
        log.warn('No refund opened for user [%s], course entitlement [%s]', enrollee.id, entitlement_uuid)
        return False
Example #56
0
    def post(self, request, *args, **kwargs):
        """
        Attempt to create the basket and enroll the user.
        """
        user = request.user
        valid, course_key, error = self._is_data_valid(request)
        if not valid:
            return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE)

        embargo_response = embargo_api.get_embargo_response(request, course_key, user)

        if embargo_response:
            return embargo_response

        # Don't do anything if an enrollment already exists
        course_id = unicode(course_key)
        enrollment = CourseEnrollment.get_enrollment(user, course_key)
        if enrollment and enrollment.is_active:
            msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username)
            return DetailResponse(msg, status=HTTP_409_CONFLICT)

        # Check to see if enrollment for this course is closed.
        course = courses.get_course(course_key)
        if CourseEnrollment.is_enrollment_closed(user, course):
            msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id)
            log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)

        # If there is no audit or honor course mode, this most likely
        # a Prof-Ed course. Return an error so that the JS redirects
        # to track selection.
        honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR)
        audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT)

        # Accept either honor or audit as an enrollment mode to
        # maintain backwards compatibility with existing courses
        default_enrollment_mode = audit_mode or honor_mode

        if not default_enrollment_mode:
            msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format(course_id=course_id)
            return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
        elif default_enrollment_mode and not default_enrollment_mode.sku:
            # If there are no course modes with SKUs, enroll the user without contacting the external API.
            msg = Messages.NO_SKU_ENROLLED.format(
                enrollment_mode=default_enrollment_mode.slug,
                course_id=course_id,
                username=user.username
            )
            log.info(msg)
            self._enroll(course_key, user, default_enrollment_mode.slug)
            self._handle_marketing_opt_in(request, course_key, user)
            return DetailResponse(msg)

        # Setup the API

        try:
            api_session = requests.Session()
            api = ecommerce_api_client(user, session=api_session)
        except ValueError:
            self._enroll(course_key, user)
            msg = Messages.NO_ECOM_API.format(username=user.username, course_id=unicode(course_key))
            log.debug(msg)
            return DetailResponse(msg)

        response = None

        # Make the API call
        try:
            # Pass along Sailthru campaign id
            campaign_cookie = request.COOKIES.get(SAILTHRU_CAMPAIGN_COOKIE)
            if campaign_cookie:
                cookie = {SAILTHRU_CAMPAIGN_COOKIE: campaign_cookie}
                if api_session.cookies:
                    requests.utils.add_dict_to_cookiejar(api_session.cookies, cookie)
                else:
                    api_session.cookies = requests.utils.cookiejar_from_dict(cookie)

            response_data = api.baskets.post({
                'products': [{'sku': default_enrollment_mode.sku}],
                'checkout': True,
            })

            payment_data = response_data["payment_data"]
            if payment_data:
                # Pass data to the client to begin the payment flow.
                response = JsonResponse(payment_data)
            elif response_data['order']:
                # The order was completed immediately because there is no charge.
                msg = Messages.ORDER_COMPLETED.format(order_number=response_data['order']['number'])
                log.debug(msg)
                response = DetailResponse(msg)
            else:
                msg = u'Unexpected response from basket endpoint.'
                log.error(
                    msg + u' Could not enroll user %(username)s in course %(course_id)s.',
                    {'username': user.id, 'course_id': course_id},
                )
                raise InvalidResponseError(msg)
        except (exceptions.SlumberBaseException, exceptions.Timeout) as ex:
            log.exception(ex.message)
            return InternalRequestErrorResponse(ex.message)
        finally:
            audit_log(
                'checkout_requested',
                course_id=course_id,
                mode=default_enrollment_mode.slug,
                processor_name=None,
                user_id=user.id
            )

        self._handle_marketing_opt_in(request, course_key, user)
        return response
Example #57
0
    def _collect_one_click_purchase_eligibility_data(self):
        """
        Extend the program data with data about learner's eligibility for one click purchase,
        discount data of the program and SKUs of seats that should be added to basket.
        """
        if 'professional' in self.data['applicable_seat_types']:
            self.data['applicable_seat_types'].append('no-id-professional')
        applicable_seat_types = set(seat for seat in self.data['applicable_seat_types'] if seat != 'credit')

        is_learner_eligible_for_one_click_purchase = self.data['is_program_eligible_for_one_click_purchase']
        skus = []
        bundle_variant = 'full'

        if is_learner_eligible_for_one_click_purchase:
            courses = self.data['courses']
            if not self.user.is_anonymous:
                courses = self._filter_out_courses_with_enrollments(courses)
                courses = self._filter_out_courses_with_entitlements(courses)

            if len(courses) < len(self.data['courses']):
                bundle_variant = 'partial'

            for course in courses:
                entitlement_product = False
                for entitlement in course.get('entitlements', []):
                    # We add the first entitlement product found with an applicable seat type because, at this time,
                    # we are assuming that, for any given course, there is at most one paid entitlement available.
                    if entitlement['mode'] in applicable_seat_types:
                        skus.append(entitlement['sku'])
                        entitlement_product = True
                        break
                if not entitlement_product:
                    course_runs = course.get('course_runs', [])
                    published_course_runs = [run for run in course_runs if run['status'] == 'published']
                    if len(published_course_runs) == 1:
                        for seat in published_course_runs[0]['seats']:
                            if seat['type'] in applicable_seat_types and seat['sku']:
                                skus.append(seat['sku'])
                                break
                    else:
                        # If a course in the program has more than 1 published course run
                        # learner won't be eligible for a one click purchase.
                        skus = []
                        break

        if skus:
            try:
                api_user = self.user
                is_anonymous = False
                if not self.user.is_authenticated:
                    user = get_user_model()
                    service_user = user.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
                    api_user = service_user
                    is_anonymous = True

                api = ecommerce_api_client(api_user)

                # The user specific program price is slow to calculate, so use switch to force the
                # anonymous price for all users. See LEARNER-5555 for more details.
                if is_anonymous or ALWAYS_CALCULATE_PROGRAM_PRICE_AS_ANONYMOUS_USER.is_enabled():
                    discount_data = api.baskets.calculate.get(sku=skus, is_anonymous=True)
                else:
                    discount_data = api.baskets.calculate.get(sku=skus, username=self.user.username)

                program_discounted_price = discount_data['total_incl_tax']
                program_full_price = discount_data['total_incl_tax_excl_discounts']
                discount_data['is_discounted'] = program_discounted_price < program_full_price
                discount_data['discount_value'] = program_full_price - program_discounted_price

                self.data.update({
                    'discount_data': discount_data,
                    'full_program_price': discount_data['total_incl_tax'],
                    'variant': bundle_variant
                })
            except (ConnectionError, SlumberBaseException, Timeout):
                log.exception(u'Failed to get discount price for following product SKUs: %s ', ', '.join(skus))
                self.data.update({
                    'discount_data': {'is_discounted': False}
                })
        else:
            is_learner_eligible_for_one_click_purchase = False

        self.data.update({
            'is_learner_eligible_for_one_click_purchase': is_learner_eligible_for_one_click_purchase,
            'skus': skus,
        })
Example #58
0
def refund_seat(course_enrollment):
    """
    Attempt to initiate a refund for any orders associated with the seat being unenrolled, using the commerce service.

    Arguments:
        course_enrollment (CourseEnrollment): a student enrollment

    Returns:
        A list of the external service's IDs for any refunds that were initiated
            (may be empty).

    Raises:
        exceptions.SlumberBaseException: for any unhandled HTTP error during communication with the E-Commerce Service.
        exceptions.Timeout: if the attempt to reach the commerce service timed out.
    """
    User = get_user_model()  # pylint:disable=invalid-name
    course_key_str = unicode(course_enrollment.course_id)
    enrollee = course_enrollment.user

    service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME)
    api_client = ecommerce_api_client(service_user)

    log.info('Attempting to create a refund for user [%s], course [%s]...', enrollee.id, course_key_str)

    refund_ids = api_client.refunds.post({'course_id': course_key_str, 'username': enrollee.username})

    if refund_ids:
        log.info('Refund successfully opened for user [%s], course [%s]: %r', enrollee.id, course_key_str, refund_ids)

        config = CommerceConfiguration.current()

        if config.enable_automatic_refund_approval:
            refunds_requiring_approval = []

            for refund_id in refund_ids:
                try:
                    # NOTE: Approve payment only because the user has already been unenrolled. Additionally, this
                    # ensures we don't tie up an additional web worker when the E-Commerce Service tries to unenroll
                    # the learner
                    api_client.refunds(refund_id).process.put({'action': 'approve_payment_only'})
                    log.info('Refund [%d] successfully approved.', refund_id)
                except:  # pylint: disable=bare-except
                    log.exception('Failed to automatically approve refund [%d]!', refund_id)
                    refunds_requiring_approval.append(refund_id)
        else:
            refunds_requiring_approval = refund_ids

        if refunds_requiring_approval:
            # XCOM-371: this is a temporary measure to suppress refund-related email
            # notifications to students and support for free enrollments.  This
            # condition should be removed when the CourseEnrollment.refundable() logic
            # is updated to be more correct, or when we implement better handling (and
            # notifications) in Otto for handling reversal of $0 transactions.
            if course_enrollment.mode != 'verified':
                # 'verified' is the only enrollment mode that should presently
                # result in opening a refund request.
                log.info(
                    'Skipping refund email notification for non-verified mode for user [%s], course [%s], mode: [%s]',
                    course_enrollment.user.id,
                    course_enrollment.course_id,
                    course_enrollment.mode,
                )
            else:
                try:
                    send_refund_notification(course_enrollment, refunds_requiring_approval)
                except:  # pylint: disable=bare-except
                    # don't break, just log a warning
                    log.warning('Could not send email notification for refund.', exc_info=True)
    else:
        log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)

    return refund_ids